反序列化漏洞还是比较重要的 , PHP反序列化漏洞又被称为PHP对象注入 , 可能会导致远程代码执行( RCE )
PHP序列化与反序列化
要想了解PHP反序列化漏洞 , 首先要知道什么是PHP序列化
定义
PHP中所有的值都可以用函数 serialize()
返回的一个包含字节流的字符串来表示 , 序列化一个对象会保存当前对象的所有变量 , 会保留类的名字 , 但是不会保存对象的方法 . 如果序列化类A的一个对象 , 那么将返回一个和类A相关 , 并且包含该对象所有变量的字符串
PHP反序列化就是通过 unserialize()
重新把字符串变回原来的值 . 如果反序列化的变量为对象( Object ) , 那么在成功重构对象后PHP会自动调用 __wakeup()
成员方法 .
简单的来说 , PHP序列化就是把代码中所有的 对象 , 类 , 数组 , 变量 , 匿名函数 ... 全部转换为一个字符串 , 提供给用户传输和存储 . 而反序列化就是把字符串重新转换为 对象 , 类 , 数组 , 变量 , 匿名函数 . 通过这种相互转换 ,从而完成 数据持久化
举个通俗易懂的例子吧
把`word`文档保存为`.docx`就是序列化操作 , 而打开`.docx`显示其中的内容 , 就是反序列化操作
几个比较重要的函数
- __construct()
在对象被创建时触发 - __destruct()
在对象被销毁时触发 - __sleep()
在对象被序列化之前触发 - __wakeup()
在对象被反序列化之前触发 - __toString()
当对象被当成字符串时触发 - __get()
当访问不可访问的属性时触发
看个demo
输出内容如下
上面这个代码初学者可能难以理解 , 来解释一下
$class = new test()
: 生成一个实例对象 , 调用构造函数__construct()
, 这点没什么说的 . 大部分面向对象的语言都是这么做的
echo $class
: 这里通过echo 把 对象$class 当做字符串输出 , 所以调用__toString()
$str = serialize()
: 序列化 对象$class . 把生成的字符串赋值给 $str , 在序列化之前先调用__sleep()
方法来清理对象 , 并且返回一个包含对象中所有要被序列化的变量名称的数组
$new_class = unserialize($str)
: 反序列化操作 , 将字符串重构成对象 . 在反序列化之前会调用__wakeup()
方法来准备对象需要的资源 , 比如重新建立在序列化期间可能已丢失的任何数据库连接当代码执行完毕后 , 会调用析构函数
__destruct()
, 销毁对象 , 因为这里生成了两个对象 , 所以析构函数会被执行两次
细心的人会发现 , 上面分别定义了三类变量 , 即 private $a
, protected $b
, public $c
, 它们经过序列化后的字符串是有区别的
private
数据类型:属性名长度:"\00类名\00属性名";数据类型:属性值长度:"属性值"
protected
数据类型:属性名长度:"\00*\00属性名";数据类型:属性值长度:属性值
public
数据类型:属性名长度:属性名;数据类型:属性值长度:属性值
属性名上的区别在构造POC时十分关键!
PHP反序列化漏洞
通过上面的内容你应该对 PHP序列化与反序列化是个什么玩意儿 有一定理解了 , 那么到底什么是反序列化漏洞?
关于PHP反序列化漏洞 , 你需要知道以下几点
1. PHP序列化给我们传递对象提供了一种简单的方法 , 反序列化的数据本质上是没有危害的
2. 但是用户可控参数的反序列化数据是有危害的
3. 当用户利用魔术方法或者其他敏感函数进行恶意操作时 , 会出现安全问题
来看个demo , 你就能初步理解了
这个代码看起来没什么问题 , 但是别忘了在 unserialize()
前会先调用 __wakeup()
来准备资源 , 它们之间没有其他的过程 , 也就是说 , 通过控制序列化字符串就可以直接来触发__wakeup()
中的内容 .
当我们在访问下面这个url时 , 会直接在目标服务器生成一个 webshell !
http://127.0.0.1/test.php?string=O:4:%22Test%22:1:{s:1:%22a%22;s:29:%22%3C?php%20eval($_POST[shell]);%20?%3E%22;}
你可以尝试连接一下 ~ 轻松拿到shell ~
上面是用户恶意利用魔术方法中的漏洞 , 那么用户是否能利用普通成员方法中的漏洞呢?
当然是可能的 , 我们可以尝试寻找相同的函数名 , 把敏感函数和类联系起来 . 下面这个例子就是普通成员方法中的漏洞
一般的工程师看到上面这个代码肯定觉得没有什么风险 , 因为唯一存在风险的函数eval()
并没有被执行 , 甚至连 class noob2
都没有被实例化
但是直觉往往是错的 , 他觉得它的代码无法调用 noob2
中的函数 , 不代表我们不可以 !
可以看到 class noob1
和 class noob2
中都存在一个 action()
的同名函数 , 而 class noob2
中的 action()
函数可以执行 $test1
中的内容 , 所以可以想方法在 new noob()
时可以调用 class noob2
中的 action()
方法
我们构造一个反序列化字符串 , 这里拿 phpinfo
做demo
O:4:"noob":1:{s:4:"test";O:5:"noob2":1:{s:5:"test1";s:10:"phpinfo();";}}
访问指定url , 即可成功调用 class noob2 中的eval()
函数
CVE-2016-7124
说到PHP反序列化漏洞 , 那就不得不提 CVE-2016-7124 , 该漏洞使得攻击者可以成功的绕过 __wakeup()
函数 .
影响版本:
PHP5 < 5.6.25
PHP7 < 7.0.10
简单的说 , 当序列化字符串中的设置的属性个数大于实际的属性个数时 , 对象属性检测就会出现异常 , 那么process_nested_data()
就会返回0 , 反序列化产生异常 , 从而不执行 __wakeup()
函数 . 在此之前对象和它的属性已经被创建 , 但紧接着对象将被破坏 , 从而执行 __destruct()
函数 , 从而产生漏洞