ThinkPHP RCE 5.x分析


前言

之前写了了php的反序列挖掘的文章,思考了去年年末到今年年初的thinkphp RCE漏洞十分的火爆,这样我就顺势写篇文章来分析一下(里面有些含糊不清的地方希望师傅们斧正)

Thinkphp 流程分析

我这次分析的第一个案例是thinkphp的5.0.12版本,这个RCE有多个payload,我会进行选择来分析,在这之前先分析一下一个thinkphp的demo是如何运行的

1.png

下断点分析

1
2
3
4
payload: 
POST /tp5012/public/index.php?s=index/index HTTP/1.1
....
s=whoami&_method=__construct&method=&filter[]=system

在开始之前先解释一下为什么大部分的payload中都会存在s这个参数,这个参数代表了什么
3.png
这里我们可以看出来这个s实际上实在兼容模式下代替了phpinfo的存在,而兼容模式是默认开启的,因此可以直接使用

我们跳过前面的加载过程直接从路由检测开始下断点
2.png
跳过一些无用的函数,进入
4.png
这里就要用到开头说的兼容模式下s代表了var_phpinfo,因此$_SERVER['PATH_INFO']就被$GET['s']覆盖了

继续向后步入,进入

1
2
3
4
5
6
7
App.class

$result = Route::check($request, $path, $depr, $config['url_domain_deploy']);

Route.class
.....
$method = strtolower($request->method());

由此我们进入啦Request.phpmethod方法
5.png

这里我们有几点需要注意

* 默认$method=false,$this->method默认为空所以我们会进入第二个判断
* 这里又有个跟s一样的地方var_method在兼容模式下就是由_method代替,所以这里就是从$POST['_method']来取得值,这里我们传入的就是`_consrtruct`
* 后面紧跟这一个动态函数执行的地方,最后这个动态函数就变为`$this->_construct($_POST)`

跟进Request.php__construct
6.png
可以看到这里是个变量覆盖,我们可以将任意这个类中的变量赋值(为最后的RCE埋下伏笔),我们继续跟进经过很多URL处理,终于到了激动人心的地方就是返回到浏览器的数据处理

1
$data = self::exec($dispatch, $config);

我们经过不断跟进,跨过各种监听,设置各种语言包,实例化类(这种payload只需要s是存在的类就可以了,关键不是s),目的是在下面,也就是为了触发我们之前设置的那个伏笔
7.png
我们继续跟进到了Request::param,我们不断进出input()函数,但是由于$name=false因此无法深入,最后在

1
return $this->input($this->param, $name, $default, $filter);

$name=""传入了input,最后进入fileterValue()

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)) {
if (false !== strpos($filter, '/')) {
// 正则过滤
if (!preg_match($filter, $value)) {
// 匹配不成功返回默认值
$value = $default;
break;
}

一个很明显的可控call_user_func,达成了我们的RCE
8.png
完结撒花(虽然只有这一个版本)

扩展思考

我们的核心流程是什么:(逆向思维)

* 在filterValue中调用了`call_user_func`
* 想调用上面的就需要调用 `input()`
* 想要调用`input()`就需要从一下中选择

9.png

* 我选中的就是我们开始payload的入口,如果我们可以找到其他地方调用`param()`函数,这样我们连payload都几乎不用该就可以rce啦

我们开App.php中不难找到

1
2
3
4
5
if (self::$debug) {
Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info');
Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info');
Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info');
}

这里就有我们想要的,但是需要self::$debug为true,不过经过test发现,我的5.0.12版本不行,但是5.0.22版本可以,diff了一下发现是参数获取有问题
10.png

5.0.12版本即使修改了app.php中的$debug依然没用,而5.0.22则修改covention.php中的app_debug,即可,同样的问题也出现在默认注册的路由captcha中,这里也有一点疑问

成功RCE
11.png

补丁分析

我们去查看官方的修复补丁

12.png

可以看到这里使用了白名单,防止我们传入恶意的method来进行实例化$Request类,最终触发RCE

参考链接

https://xz.aliyun.com/t/3845#toc-0
https://www.cnblogs.com/iamstudy/articles/thinkphp_5_x_rce_1.html