一直对 PHP
反序列化方面的内容似懂非懂,这次终于想着要好好梳理一下这方面的内容。
ToC
概念
我们知道,PHP
有 serialize
和 unserialize
这一对函数,前者负责序列化,后者负责反序列化。
序列化负责的是将 PHP
的对象序列化为字符串,而反序列化负责的则是将字符串转化回对象。
序列化
分隔符(结束符)
PHP
反序列化中最常见的分隔符就是分号(;
)。在需要表示多个值时,都会使用到分号以间隔数据。
基本类型
基本类型的转化关系如下所示:
PHP 值 | 序列化值 |
---|---|
NULL | N |
true | b:1 |
false | b:0 |
42 (整数) | i:42 |
114.514 (浮点数) | d:114.514 |
"rua" (字符串) | s:3:"rua" |
resource (资源类型) | i:0 |
数组类型
PHP
的数组类型实质是键值对(key-value
)的集合。
序列化开头以 a:
开始,表面是数组类型。
然后连接上数组的长度,以 len=3
的数组为例, 3:
。
接下来通过一对大括号表示包裹的内容,开始——{
。
然后是数组的内容,首先是 key
,比如 i:0
;接下来是 value
,比如 s:"rua"
重复多次,最后结束——}
。
最后对应 ["rua", "233", "dd"]
的序列化结果就是 a:3:{i:0;s:3:"rua";i:1;s:3:"233";i:2;s:2:"dd";}
。
Class 类型
我们以这个类为例:
可以发现,这个类里同时存在着 public
、private
和 protected
的成员变量。PHP
的序列化对这三种成员的序列化方式不同。
前缀
和数组类型一样,Class
类型也存在前缀。前缀是 O:
,后面紧跟的是类的名称:4:"Test"
。
包裹符
和数组类型一样,Class
类型也需要通过一对大括号来包裹其成员。
public 成员
public
成员的序列化过程最简单,和数组类型的 key-value
相同。
private 成员
private
成员序列化时需要在 key
前增加 \0Test\0
。其中 Test
是类名,而 \0
则是空字符。
protected 成员
protected
成员在序列化时需要在 key
前增加 \0*\0
。
结果
最后的结果如下:
实现 Serializable 接口的类
// TODO,没遇到过
魔术方法
PHP
中以双下划线(__
)开头的方法称为魔术方法。在序列化中,用到的魔术方法为 __sleep
;而在反序列化中,用到的魔术方法为 __wakeup
。
在反序列化的利用中,常常还会用到其他魔术方法。需要用到时查阅文档即可。
反序列化漏洞
基本
最基本的就是在反序列化之后执行了危险操作。这种类型的利用手段就是通过代码审计,找到利用方法即可。
字符逃逸
字符逃逸是指在序列化有,修改了反序列化的内容,导致代表字符串长度的数据和字符串的实际长度不相符。
根据修改后的长度比修改前长/短,可以分为变短和变长两类。
变长
字符串变长意味着可以通过一个字符串字段实现反序列化内容的逃逸。以下列代码为例:
最后的 var_dump
输出为:
可以看到,最后 c
的值被覆盖成了 dddd
。观察 payload
,不难发现 xxxxxxxxxxxxxxxxxxxxxx";s:1:"c";s:4:"dddd";}
的长度就是 44
,但由于 x
被替换成了 xx
,导致需要反序列化的字符串变成了 O:4:"Test":3:{s:1:"a";i:1;s:1:"b";s:44:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";s:1:"c";s:4:"dddd";}";s:1:"c";N;}
。而当大括号闭合后,反序列化结束。因此最后的 ";s:1:"c";N;}
被忽略,逃逸成功。
变短
字符串变短意味着需要通过两个字符串字段实现内容的逃逸。以这次校选拔赛的 web6_serialize_2
为例:
可以看到,题目替换了 flag
字符串,将长度为 4 的字符串替换为长度为 0,导致字符逃逸。
不难发现,我们需要构造的是 B
的实例,同时满足 filename == 'flag.php'
。字符串变短意味着反序列化过程在会把当前字段之后的部分当作当前字段的值处理,因此需要配合另一个字段实现内容的补全。
我们在需要在 a
中放入 n 个 flag
,而对于正常的反序列化字符串,我们需要补全 ";s:8:"password";s:1:
。由于一次减少的字符是 4 个,因此我们需要将补全的字符串长度整理为 4 的倍数。这里我们整理成 ";s:8:"password";s:1000:
,即 24 字符,在 a
中放入 6 个 flag
。
而在 b
中,我们需要补全之前被填充而不被记为反序列化的部分,并且在 password
中存入 B
的实例。这里需要绕过 filename == 'flag.php'
,因此这里通过 0 == 'string'
的弱类型特性进行绕过。
最终的 payload
如下所示: