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 | define('APP_PATH', __DIR__ . '/../application/'); |
直接require了./../thinkphp/start.php
,跟入该文件
1 |
|
进入App.php
的run()
方法,该方法的实现主要步骤可简化为:
1 | public static function run(Request $request = null) |
路由检测位于routeCheck($request,$config)
中,跟入该函数
1 | public static function routeCheck($request, array $config) |
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,对应分割结果分别匹配为模块|控制器|操作|操作参数,
因此最后获取到的路由为
回到App.php
中,
1 | $data = self::exec($dispatch, $config); |
这行操作是整个RCE实现的关键。我们跟入exec
方法的实现
1 | protected static function exec($dispatch, $config) |
跟入module
方法
1 | public static function module($result, $config, $convert = null) |
进入Loader::controller
方法
1 | public static function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '') |
跟入App::invokeClass
方法,
1 | public static function invokeClass($class, $vars = []) |
该方法使用php的反射机制返回指定类的一个对象,因此由我们的poc,Loader::controller
返回了\think\app
类的一个实例。
继续回到App::module
方法
1 | $action = $actionName . $config['action_suffix']; |
可以看到App::module
方法之后会判断之前生成的实例是否有对应的方法,存在的话便会设置$call
变量为[\think\App类的实例,'invokefunction']
,最后调用self::invokeMethod($call,$vars)
。跟入该方法
1 | public static function invokeMethod($method, $vars = []) |
跟入self::bindParams
,该方法用于获取最后执行的函数的参数
1 | private static function bindParams($reflect, $vars = []) |
默认情况下Config::get('url_param_type')
为0,因此$vars
被设置为Request::instance()->param()
,在我们的poc中$vars
即
即为我们的请求参数。之后通过判断$reflect
的方法中需要的参数最后返回参数列表$args
回到invokeMethod
方法,
1 | return $reflect->invokeArgs(isset($class) ? $class : null, $args); |
这里调用了$reflect
的invokeArgs
方法,即通过反射调用/think/App
类的invokefunction
方法。
1 | public static function invokeFunction($function, $vars = []) |
可以看到最后invokeFunction()
相当于直接调用call_user_func_array("phpinfo",[1])
可见整个RCE的原因便是由于thinkphp在获取控制器时过滤不足导致可以任意生成类的实例调用指定的方法而导致的。怎样获取其他的可用的RCE的poc?我们可以在Loader::controller
方法中添加一行代码:
1 | $t=get_declared_classes(); |
在这行代码后的代码处下断点调试
可以看到这些类都是tp中可用的用来RCE的类,我们只需要再多研究便可发现其他的利用链。
修复方法也是很简单粗暴,只需要过滤掉反斜杠即可。