一、PHP 反序列化漏洞基础
1.1PHP 序列化
概念:序列化:是将变量转换为可保存或传输的字符串的过程;实现函数是 serialize()反序列化:就是在适当的时候把这个字符串再转化成原来的变量使用,就是序列化的逆过程。实现函数是 unserialize()序列化和反序列化结合起来,可以轻松地存储和传输数据,使程序更具维护性。
序列化格式:
| |
|---|
| O:length:class name:attribute number:{attr1;value1;attr2;value2;} |
| a:number:{key1;value1;key2;value2;} |
| |
| |
| b:value; //(0=False,1=True) |
| |
| |
例子:对象的序列化
to
to
对象字段(属性)名的序列化规则:var 和 public:var 和 public 声明的字段都是公共字段,因此它们的字段名的序列化格式是相同的。公共字段的字段名按照声明时的字段名进行序列化,但序列化后的字段名中不包括声明时的变量前缀符号 $。protected:声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。保护字段的字段名在序列化时,字段名前面会加上\0*\0 的前缀。这里的\0 表示 ASCII 码为 0 的字符,属于不可见字符,因此该字段的长度会比可见字符长度大 3。private:声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。私有字段的字段名在序列化时,字段名前面会加上\0<declared classname>\0 前缀。这里 <declared classname> 表示的是声明该私有字段的类的类名,而不是被序列化的对象的类名。因为声明该私有字段的类不一定是被序列化的对象的类,而有可能是它的祖先类。
数组的序列化:
to
to
格式由类型标识 + 数据长度 + 内容组成,核心标识:
s:n:"xxx" :字符串,n 为字符长度,如 s:4:"name"
i:n :整数,n 为数值,如 i:20
a:n:{...} :数组,n 为元素个数,大括号内为键值对
O:n:"类名"m:{...} :对象,n 为类名字符数,m 为属性个数
N; :NULL
b:0/1; :布尔值(假/真)
私有属性特殊格式:私有属性序列化后,属性名会拼接 \0 类名\0 (不可见空字符),长度包含空字符,如 User 类的 $pwd 会变成 \0User\0pwd (长度 8)。
1.2 PHP 反序列化漏洞
反序列化漏洞:也叫对象注入,就是当程序在进行反序列化时,会自动调用一些函数,但是如果传入函数的参数可以被用户控制的话,用户可以输入一些恶意代码到函数中,从而导致反序列化漏洞。可以理解为程序在执行 unserialize()函数是,自动执行了某些魔术方法(magic method),而魔术方法的参数被用户所控制(通过控制属性来控制参数),这就会产生安全问题。
漏洞利用条件:
unserialize()函数的参数可控。
存在可利用的魔术方法。
魔术方法内调用了危险函数,且危险函数的参数可控(由对象的属性赋值)。
出现场景:
代码审计;
服务器源码泄露后挖掘漏洞;
CTF-WEB 题目。
1.3 属性赋值
要利用反序列化漏洞,必须向 unserialize()函数传入构造的序列化数据(定义合适的属性值)。那么,如何构造序列化数据呢?直接写序列化数据显示是不合适的,因为序列化数据不符合人类直观,很容易出错。实际上,都是利用 serialize()函数来生成序列化数据的。一般输出都会使用 urlencode 函数对 URL 编码然后输出
进行 URL 编码的原因:
原始的序列化数据可能存在不可见字符;
如果不进行编码,最后输出的结果是片段的,不是全部的,会有类似截断导致结果异常,所以需要进行 url 编码。
对属性赋值主要有 3 种方法:
直接在属性中赋值:优点是方便,缺点是只能赋值字符串
to
外部赋值:优点是可以赋值任意类型的值,缺点是只能操作 public 属性。
to
构造方法赋值(万能方法):优点是解决了上述的全部缺点,缺点是有点麻烦
to
1.4 魔术方法
魔术方法是一种特殊的方法,在某些情况下会自动调用。魔术方法的命名是以两个下划线开头的,常见的魔术方法有:
to
二、POP 链
2.1 概念
POP 链:POP(面向属性编程)链是指从现有运行环境中寻找一系列的代码或指令调用,然后根据需求构造出一组连续的调用链。反序列化利用就是要找到合适的 POP 链。其实就是构造一条符合原代码需求的链条,去找到可以控制的属性或方法,从而构造 POP 链达到攻击的目的。寻找 POP 链的思路:
寻找 unserialize()函数的参数是否可控,运用反推法,寻找起始与重点函数;
寻找反序列化想要执行的目标函数,重点寻找魔术方法(比如__wakeup()和__destruct());
一层一层地研究目标在魔术方法中使用的属性和调用的方法,看看其中是否有我们可控的属性和方法;
根据我们要控制的属性,构造序列化数据,发起攻击。
2.2 畸形序列化字符串
2.2.1 认识畸形序列化字符串
畸形序列化字符串就是故意修改序列化数据,使其与标准序列化数据存在个别字符的差异,达到绕过一些安全函数的目的。应用领域:
绕过__wakeup()
快速析构(fast destruct):绕过过滤函数,提前执行__destruct
2.2.2 绕过__wakeup
由于使用 unserialize()函数后会立即触发__wakeup,为了绕过__wakeup 中的安全机制,可以用修改属性数量的方式绕过__wakeup 方法。受影响版本:
php5.0.0 - php5.6.25 php7.0.0 - php7.0.10
绕过方法:1.反序列化时,修改对象的属性数量,将原数量 +n,那么__wakeup 方法将不再调用。2.增加真实属性的个数。
2.3.3 快速析构
快速析构的原理:当 php 接收到畸形序列化字符串时,PHP 由于其容错机制,依然可以反序列化成功。但是,由于你给的是一个畸形的序列化字符串,总之他是不标准的,所以 PHP 对这个畸形序列化字符串得到的对象不放心,于是 PHP 就要赶紧把它清理掉,那么就触发了他的析构方法(__destruct())。应用场景:某些题目需要利用__destruct 才能获取 flag,但是__destruct 是在对象被销毁时才触发(执行顺序太靠后),__destruct 之前会执行过滤函数,为了绕过这些过滤函数,就需要提前触发__destruct 方法。畸形字符串的构造1.改掉属性的个数2.删掉结尾的}
三、PHP 指针与反序列化字符逃逸
1.1 指针
用&符号可以进行指针引用,类似于 C 语言中的指针。
2. 反序列化字符逃逸
2.1 字符逃逸
字符逃逸本质:对序列化字符串进行不等长的字符串替换,导致本来属于普通字符串的一部分字符串变成了序列化的一部分,或者导致本来不属于字符串的一部分变成了字符串的一部分,进而造成了序列化数据的错乱,导致了对象注入。思路:
写出基本序列化
写出注入的对象
分析是长到短还是短到长的替换,决定要把对象注入到什么地方
算清楚替换的差值,计算需要吃掉或挤出(逃逸)的字符串的长度,保证这个长度是替换的差值的整数倍,如果不能保证,加字符串
构造替换,对象注入。
类型:长到短的替换:在第一个元素进行替换,进而吃掉第二个元素的约束,第二个元素就逃逸出来了。短到长的替换:直接在替换位点后面跟上注入的对象,注入对象就可以逃逸出来。