反序列化
首先,反序列化(unserialize)对应序列化(serialize),通俗理解就是后者将对象转化为字符串,前者将字符串转化为对象,但是由于前者的可操作性带来了危害。
php反序列化漏洞主要操作的是对象,也可以称为php对象注入漏洞,当调用unserialize函数对一个对象进行反序列时,如果该对象内部使用了魔法函数,并且魔法函数中包含了一些危险可控的操作,那么攻击者可能利用这点构造一段恶意代码执行,这就是反序列化漏洞。
php中提供了一些魔法函数,这些函数可以直接使用,不需要声明,常用的魔法函数如下:
方法名 | 调用条件 |
---|---|
__call | 调用不可访问或不存在的方法时被调用 |
__callStatic | 调用不可访问或不存在的静态方法时被调用 |
__clone | 进行对象clone时被调用,用来调整对象的克隆行为 |
__constuct | 构建对象的时被调用;(但在unserialize()时是不会自动调用的)。 |
__debuginfo | 当调用var_dump()打印对象时被调用(当你不想打印所有属性)适用于PHP5.6版本 |
__destruct | 明确销毁对象或脚本结束时被调用; |
__get | 读取不可访问或不存在属性时被调用 |
__invoke | 当以函数方式调用对象时被调用 |
__isset | 对不可访问或不存在的属性调用isset()或empty()时被调用 |
__set | 当给不可访问或不存在属性赋值时被调用 |
__set_state | 当调用var_export()导出类时,此静态方法被调用。用__set_state的返回值做为var_export的返回值。 |
__sleep | 当使用serialize时被调用,当你不需要保存大对象的所有数据时很有用 |
__toString | 当一个类被转换成字符串时被调用 |
__unset | 对不可访问或不存在的属性进行unset时被调用 |
__wakeup | 当使用unserialize时被调用,可用于做些对象的初始化操作 |
这里先以_toString()
函数为例:
1 |
|
我来解说下,同时也是传达思想。test1对象
内部使用了_toString()
魔法函数,并且在_toString()
函数内部使用了file_get_contents
函数读取了属性$file_name
指向文件,但_toString
函数只有在test1对象
当做字符串使用的情况下才会被调用。Test2对象
中调用了test2_unserialize
函数对test对象
进行反序列化,并将test1对象
输出,那么此时就会调用test1对象
里的_toString
函数读取文件。
test.txt文件里的内容就会被输出。假设$test1_str变量里的内容如果是可控的话,那么就可以将test.txt内容进行替换,进行任意文件读取。这时,就有一个反序列化pop利用链。
2.反序列化pop利用链
pop漏洞利用链代码审计:pop(面向对象编程)链的构造是寻找程序当中环境已经定义了或者能够动态加载的对象属性(函数方法),将一些可能的调用组合在一起形成一个完整的,具有目的性的操作。
反序列化漏洞的挖掘可以从以下方面进行:
进行反序列化的数据点可控
反序列化类中有魔法方法,并且魔法方法中有敏感操作
魔法方法中没有敏感操作,但是其对象调用了其它类中同名函数,可以通过构造pop链利用
unserialize函数(变量可控)–>__wakeup()魔术方法–>__tostring()魔术方法–>__get魔术方法–>__invoke魔术方法–>触发Read类中的file_get方法–>触发file_get_contents函数读取flag.php
反序列化的常见起点
__wakeup 一定会调用
__destruct 一定会调用
__toString 当一个对象被反序列化后又被当做字符串使用
反序列化的常见中间跳板:
__toString 当一个对象被当做字符串使用
__get 读取不可访问或不存在属性时被调用
__set 当给不可访问或不存在属性赋值时被调用
__isset 对不可访问或不存在的属性调用isset()或empty()时被调用。形如 $this->$func();
反序列化的常见终点:
__call 调用不可访问或不存在的方法时被调用
call_user_func 一般php代码执行都会选择这里
call_user_func_array 一般php代码执行都会选择这里
主要还是三点:
起点
跳板
代码执行
这里可以了解到,反序列化unserialize会调用__wakeup()函数进行对象初始化,我们就要想办法绕过此函数,这里有一个点当传入对象大于类的对象时可以绕过weakup函数
,但是这对php版本是有要求的。PHP2-php5.6.25和php7-php7.0.10这两个区间内可行。
文件的输出与输入
先介绍序列化serialize,对象转化为字符串。
1 |
|
O:4: 这是一个object对象,长度为4
“Name”:2: 代表名字为Name,Name有两个属性
{
s:14:“Nameusername”; string类型,属性名长度为14,属性名是username //这里为什么是14,为什么属性名是username而不是Nameusername后面会说
s:5:“admin”; string类型,参数值长度5,参数值是admin
s:14:“Namepassword”; string类型,属性名长度为14,属性名是password
i: 100; int类型,参数值是100
}
这时序列化的输出,反序列化我们只需要按照格式创建自己需要的字符串就ok。
再扩充下php序列化基础格式
boolean
b:;
b:1; //True
b:0; //False
integer
i:;
i:1; // 1
i:-3; // -3
double
d:;
d:1.2345600000000001; // 1.23456(php弱类型所造成的四舍五入现象)
NULL
N; //NULL
string
s::””; //前面已经有介绍
array
a::{key,value pairs};
a{s”key1”;s”value1”;s”value2”;} // array(“key1” => “value1”, “key2” => “value2”)
关于对象
说起对象,emm,看代码,先要了解public、private、protected这三种。
其中public正常使用就行,不用添加也不用减少
protected 声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。因此保护字段的字段名在序列化时,字段名前面会加上\0*\0
的前缀。这里的 \0 表示 ASCII 码为 0 的字符(不可见字符),而不是 \0 组合。
这也许解释了,为什么如果直接在网址上,传递\0*\0username会报错,因为实际上并不是\0,只是用它来代替ASCII值为0的字符。必须用python传值才可以。
1 |
|
private 声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,类名和字段名前面都会加上\0的前缀。字符串长度也包括所加前缀的长度。其中 \0 字符也是计算长度的。
1 |
|
这里的\0
也可以写为%00,这是空的意思,因为输出的时候不会输出空。
注意:这里的双引号有必要时需要url编码,论环境而定。