# 起:
反序列化作为 CTF 比赛的热点,从从 Orange 在 2017 年的 hitcon 出了一个 0day 的 php phar:// 反序列化给整个安全界开启了新世界的大门以后,就开始持续升温。
要想明白反系列化到底是个啥玩意,
# 首先得知道什么是序列化?
以下是系统的说法:
序列化就是将数据转化成一种可逆的数据结构,自然,逆向的过程就叫做反序列化。
# 为什么 php 要有序列化?
这其实是为了解决 PHP 对象传递的一个问题,因为 PHP 文件在执行结束以后就会将对象销毁,那么如果下次有一个界面恰好要用到刚刚销毁的对象就会束手无策,总不能永远不让它销毁吧?
于是人们就想出了一种能长久保存对象的方法,这就是 php 序列化的由来。
所以序列化的目的就是为了方便数据的传输和存储,这也就得出了 xml、json、字节数组等序列化格式。
在 PHP 中,序列化和反序列化一般用做缓存,比如 session 缓存,cookie 等。
序列化示例:
<?php | |
class test{ | |
public $name = 'P2hm1n'; | |
private $sex = 'secret'; | |
protected $age = '20'; | |
} | |
$test1 = new test(); | |
$object = serialize($test1); | |
print_r($object); | |
?> |
关键函数 serialize (): 将 PHP 中创建的对象,变成一个字符串
private 属性序列化的时候格式是 %00 类名 %00 成员名
protected 属性序列化的时候格式是 %00*%00 成员名
关键要点:
在 Private 权限私有属性序列化的时候格式是 %00 类名 %00 属性名
在 Protected 权限序列化的时候格式是 %00*%00 属性名
# 举个例子:
<?php | |
class test{ | |
private $test1="hello"; | |
public $test2="hello"; | |
protected $test3="hello"; | |
} | |
$test = new test(); | |
echo serialize($test); | |
// O:4:"test":3:{s:11:" test test1";s:5:"hello";s:5:"test2";s:5:"hello";s:8:" * test3";s:5:"hello";} | |
?> |
test 类定义了三个不同类型 (私有,公有,保护) 但是值相同的字符串,序列化输出的值不相同 O:4:"test":3:
通过对网页抓取输出是这样的 O:4:"test":3:
private 的参数被反序列化后变成 \00test\00test1 public 的参数变成 test2 protected 的参数变成 \00*\00test3
你可能会发现这样一个问题,假如你这个类定义了那么多方法,怎么把对象序列化了以后全都丢了?你看你整个序列化的字符串里面全是属性,就没有一个方法,这是为啥?
请记住,序列化他只序列化属性,不序列化方法,这个性质就引出了两个非常重要的话题:
1) 我们在反序列化的时候一定要保证在当前的作用域环境下有该类存在
这里不得不扯出反序列化的问题,这里先简单说一下,反序列化就是将我们压缩格式化的对象还原成初始状态的过程(可以认为是解压缩的过程),因为我们没有序列化方法,因此在反序列化以后我们如果想正常使用这个对象的话我们必须要依托于这个类要在当前作用域存在的条件。
(2) 我们在反序列化攻击的时候也就是依托类属性进行攻击
因为没有序列化方法嘛,我们能控制的只有类的属性,因此类属性就是我们唯一的攻击入口,在我们的攻击流程中,我们就是要寻找合适的能被我们控制的属性,然后利用它本身的存在的方法,在基于属性被控制的情况下发动我们的发序列化攻击(这是我们攻击的核心思想,这里先借此机会抛出来,大家有一个印象)
# 反序列化示例:
<?php | |
$object = '经过序列化的字符串'; | |
$test = unserialize($object1); | |
print_r($test3); | |
?> |
关键函数 unserialize (): 将经过序列化的字符串转换回 PHP 值
当有 protected 和 private 属性的时候记得补齐空的字符串。
# 为什么会产生反序列化漏洞?
# 概念解释
PHP 反序列化漏洞又叫做 PHP 对象注入漏洞,是因为程序对输入数据处理不当导致的.
反序列化漏洞的成因在于代码中的 unserialize () 接收的参数可控,从上面的例子看,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击。
# 需要具备反序列化漏洞的前提:
必须有 unserailize() 函数 | |
unserailize() 函数的参数必须可控(为了成功达到控制你输入的参数所实现的功能,可能需要绕过一些魔法函数 |
# 反序列化中常见的魔术方法
让我们看看吧
PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,除了上述魔术方法,建议定义函数不要以 __为前缀。 常见的魔法方法如下:
__construct(),类的构造函数 | |
__destruct(),类的析构函数 | |
__call(),在对象中调用一个不可访问方法时调用 | |
__callStatic(),用静态方式中调用一个不可访问方法时调用 | |
__get(),获得一个类的成员变量时调用 | |
__set(),设置一个类的成员变量时调用 | |
__isset(),当对不可访问属性调用isset()或empty()时调用 | |
__unset(),当对不可访问属性调用unset()时被调用。 | |
__sleep(),执行serialize()时,先会调用这个函数 | |
__wakeup(),执行unserialize()时,先会调用这个函数 | |
__toString(),类被当成字符串时的回应方法 | |
__invoke(),调用函数的方式调用一个对象时的回应方法 | |
__set_state(),调用var_export()导出类时,此静态方法会被调用。 | |
__clone(),当对象复制完成时调用 | |
__autoload(),尝试加载未定义的类 | |
__debugInfo(),打印所需调试信息 |
(1) __construct ():当对象创建时会自动调用 (但在 unserialize () 时是不会自动调用的)。
(2) __wakeup () :unserialize () 时会自动调用
(3) __destruct ():当对象被销毁时会自动调用。
(4) __toString (): 当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用
(5) __get () : 当从不可访问的属性读取数据
(6) __call (): 在对象上下文中调用不可访问的方法时触发
其中特别说明一下第四点:
这个 __toString 触发的条件比较多,也因为这个原因容易被忽略,常见的触发条件有下面几种
(1)echo ($obj) / print($obj) 打印时会触发 | |
(2)反序列化对象与字符串连接时 | |
(3)反序列化对象参与格式化字符串时 | |
(4)反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型) | |
(5)反序列化对象参与格式化SQL语句,绑定参数时 | |
(6)反序列化对象在经过php字符串函数,如 strlen()、addslashes()时 | |
(7)在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用 | |
(8)反序列化的对象作为 class_exists() 的参数的时候 |
有了这个,mama 再也不用担心遗漏了!
在我们的攻击中,反序列化函数 unserialize () 是我们攻击的入口,也就是说,只要这个参数可控,我们就能传入任何的已经序列化的对象(只要这个类在当前作用域存在我们就可以利用),而不是局限于出现 unserialize () 函数的类的对象,如果只能局限于当前类,那我们的攻击面也太狭小了,这个类不调用危险的方法我们就没法发起攻击。
但是我们又知道,你反序列化了其他的类对象以后我们只是控制了是属性,如果你没有在完成反序列化后的代码中调用其他类对象的方法,我们还是束手无策,毕竟代码是人家写的,人家本身就是要反序列化后调用该类的某个安全的方法,你总不能改人家的代码吧,但是没关系,因为我们有魔法方法。
魔法正如上面介绍的,魔法方法的调用是在该类序列化或者反序列化的同时自动完成的,不需要人工干预,这就非常符合我们的想法,因此只要魔法方法中出现了一些我们能利用的函数,我们就能通过反序列化中对其对象属性的操控来实现对这些函数的操控,进而达到我们发动攻击的目的。
但还有个很重要的一个点,那就是
# 魔法函数是有先后顺序的
我们可以写个代码验证一下:
<?php | |
class pompom{ | |
private $name = "pompom"; | |
function __construct(){ | |
echo "__construct"; | |
echo "</br>"; | |
} | |
function __sleep(){ | |
echo "__sleep"; | |
echo "</br>"; | |
return array("name"); | |
} | |
function __wakeup(){ | |
echo "__wakeup"; | |
echo "</br>"; | |
} | |
function __destruct(){ | |
echo "__destruct"; | |
echo "</br>"; | |
} | |
function __toString(){ | |
return "__toString"."</br>"; | |
} | |
} | |
$pompom_old = new pompom(); | |
$data = serialize($pompom_old); | |
file_put_contents("serialize-3.txt", $data); | |
$pompom_new = unserialize($data); | |
print($pompom_new); |
输出结果:
__construct
__sleep
__wakeup
__toString
__destruct
__destruct
就是提示一下这里 __destruct 了两次说明当前实际上有两个对象,一个就是实例化的时候创建的对象,另一个就是反序列化后生成的对象。
最后说明以下
# PHP 反序列化标识符的含义
a - array
b - boolean
d - double
i - integer
o - common object
r - reference
s - string
C - custom object
O - class
N - null
R - pointer reference
U - unicode string
序列化:把复杂的数据类型压缩到一个字符串中 数据类型可以是数组,字符串,对象等 函数 : serialize ()
反序列化:恢复原先被序列化的变量 函数: unserialize ()
<?php | |
$test1 = "hello world"; | |
$test2 = array("hello","world"); | |
$test3 = 123456; | |
echo serialize($test1); // s:11:"hello world"; 序列化字符串 | |
echo serialize($test2); //a:2:{i:0;s:5:"hello";i:1;s:5:"world";} 序列化数组 | |
echo serialize($test3); // i:123456; | |
?> | |
<?php | |
class hello{ | |
public $test4 = "hello,world"; | |
} | |
$test = new hello(); | |
echo serialize($test); // O:5:"hello":1:{s:5:"test4";s:11:"hello,world";} 序列化对象 首字母代表参数类型 O->Objext S->String... | |
?> |