PHP反序列化
0x01 前言
基础漏洞梳理
0x2 概念
序列化和反序列化
php中有两个函数 serialize() 和 unserialize()。
serialize()
serialize() 函数用于序列化对象或数组,并返回一个字符串。
测试代码:
代码中定义了一个类 pure,pure类中有一个var类变量 $test,即相当于java的public类型变量,php规定类属性必须定义为公有,受保护,私有之一,php中的var类型就相当于public类型。
关于"->" 的理解,功能类似于指针,这个符号相当于汉语里的“的”这个字的意思,$this->name 意思就是当前对象中的name变量。
然后将类pure实例化为对象,然后对变量$class1也就是被实例化的类进行序列化操作。然后输出序列化后的结果。
这里的O代表存储的是对象(object),假如你给serialize()传入的是一个数组,那它会变成字母a。4表示对象名有七个字符。"pure" 表示对象的名称。1 表示对象内有一个变量。{s:4:"test";s:3:"123";} 中,s表示字符串,4表示该字符串的长度,"test"为该字符串的名称,后面的和上一个一样。
unserialize()
此函数为serialize函数的逆运算,也就是将被序列化的字符串还原。
示例代码:
代码定义了一个类 pure 然后,类中定义了一个变量test。然后定义了变量class2,此变量为类pure序列化后的字符串,然后使用unserialize()函数将变量$class2反序列化操作,然后输出结果。
我们看一下执行结果:
可见序列化后的字符串被还原为了对象。(=>这个符号左边代表键,右边代表值)
反序列化漏洞的产生原因
和大多数漏洞类似,只要传给unserialize()的参数可控时,我们只需通过传入一个精心构造的序列化字符串,从而控制对象内部的变量甚至是函数。
举一个例子:
此时我们可以控制参数test,我们尝试构造payload:
字符串经过反序列化后还原为了命令
0x03 利用构造函数和成员函数。
魔法函数 Magic function
php中有一类特殊的方法叫做“Magic function”,如以下:
- 构造函数_construct():当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用的。
- 析构函数__destruct():当对象被销毁时会自动调用。
- __wakeup() :当使用unserialize()恢复对象时,会自动调用。
测试代码:
可见_wakeup()和_destruct()被调用。
利用
__wakeup() 或__destruct()
由前可以看到,unserialize()后会导致__wakeup() 或__destruct()的直接调用,中间无需其他过程。因此最理想的情况就是一些漏洞/危害代码在__wakeup() 或__destruct()中,从而当我们控制序列化字符串时可以去直接触发它们。这里针对 __wakeup() 场景做个实验。假设index源码如下:
基本思路为:本地搭建好环境,通过serialize()得到我们想要的序列化字符串,之后传到index。通过源码可知,把对象中的test变量赋值为"<?php phpinfo();?>"后,再调用unserialize()时会通过__wakeup()把test的值写入shell.php中间。所以编写脚本:
运行得到序列化后的字符串
提交至index.php
其他magic function的利用
如果一次unserialize()中并不会直接调用的魔术函数,比如前面说的__construct(),是不是就无法利用了呢,并不是,有时候反序列化一个对象时,由它调用的_wakeup(0中又去调用了其他的对象,由此便可以溯源而上,利用每一次的"gadget"找到漏洞点。
测试代码:
在代码中,我们可以控制的参数为test变量,也就是说我们可以将构造好的序列化字符串赋值给test,然后test被接收后会执行unserialize()操作, 此时pure类中的test属性已经被我们修改, 且当unserialize()函数执行时会自动调用成员函数__wakeup(),__wakeup 函数将pwn类实例化为对象,且将当前对象的test属性的值传到了pwn对象中,然后pwn对象中的__construct()方法接收test的值并执行了写文件的操作。最后便达成了我们想要的结果。
payload:
利用普通成员方法
前面谈到的利用都是基于"自动调用"的magic function。但当漏洞/危险代码存在类的普通方法中,就不能指望通过 "自动调用" 来达到目的了。这时的利用方法如下,寻找相同的函数名,把敏感函数和类联系在一起。
测试代码:
代码接收test参数后,会对其进行反序列化操作,同时pure类被实例化为对象,我们之前说过,在创建对象的时候__construct()会被自动调用,所以pure类中的__construct()在pure被实例化的时候调用了,在 __construct() 方法中又对类pwnx进行了实例化,且在结束本方法后执行了 __destruct() 方法,此方法调用了pwnx类中的action方法,同时输出了一串字符串。
我们观察源码,可见类pwny中,包含eval()函数,所以这就是我们的漏洞点,但是代码中的pop链并未涉及到此类,所以需要我们构造可以触发eval函数的序列化字符串。
我们的目的是可以操作pwny类中的eval()函数,所以我们必须实例化pwny类,因此我们的序列化字符串中必须包含实例化pwny类的操作,且需要将构造的命令传入action方法中,由于函数并没有设置接收的参数,所以我们必须将pwny对象中action方法中的test2属性赋值为我们要执行的命令。最后我们只需将初始类实例化即可将我们的代码运行起来。最后将结果进行反序列化即可。
代码如下:
我们将pure类实例化后的值序列化,此时__construct()方法会被调用,此方法的代码块为:将pwny类实例化,而pwny对象中的test2属性被我们设置为想要执行的命令,然后,这一串经过序列化后的字符串会在源码执行还原对象的操作,此时还原了对象pure,且将实例化对象由实例化 pwnx类,变为了实例化 pwny 类,然后在index.php内会继续调用 __destruct() 方法,此时就会调用当前对象的action方法,此时被调用的不再是pwnx的action方法,而是pwny的action方法,由于我们将test2属性赋值,所以代码便会执行我们的构造的请求。
0x4 结语
end!