ThinkPHP 5.x RCE分析

第一次進行代碼量這麼大的分析,記錄一下,個人感覺新手真的不適應這種,應該找點小一點的cms去分析,如果不懂MVC架構真的可能會懵。。。

前言

在分析這個之前還看了兩篇tp5的RCE漏洞,這兩個洞都是很相似的,都是利用一個可控的變量dispatch去實現到最後還是構造出回調函數,可以學習一下,感覺這裏面的思路就是本文分析漏洞的來源

https://xz.aliyun.com/t/3845

https://xz.aliyun.com/t/3845

我這裏已tp 5.0.22爲例子,環境是phpstudy搭建的

補丁

影響版本
THINKPHP 5.0.5-5.0.22

THINKPHP 5.1.0-5.1.30

5.0.x補丁地址:https://github.com/top-think/framework/commit/b797d72352e6b4eb0e11b6bc2a2ef25907b7756f

kDf8x0.png

5.1.x補丁地址:https://github.com/top-think/framework/commit/802f284bec821a608e7543d91126abc5901b2815

漏洞分析

補丁中加了正則限制了控制器的自定義初始化

payload:

localhost/tp52/public/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=dir

根據補丁下的點,動態跟蹤一下是否是因爲controller沒有做好過濾而實體化,確實是如此

kD51Gd.png

根據傳進去的payload,控制器以及下面的方法都會發生對應的變化,下面就可以分析一下攻擊鏈的流程

可以從入口文件一級一級跟蹤,進入到App.php中,這裏應該涉及到一個開發的知識,在App.php中,會根據請求的URL調用routeCheck進行調度解析在App.php中,會根據請求的URL調用routeCheck進行調度解析獲得到$dispatch,所以payload是一定要經過那裏的,可以在那裏加斷點進行調試

定位到/thinkphp/library/think/App.php:116

$dispatch = self::$dispatch;

            // 未設置調度信息則進行 URL 路由檢測
            if (empty($dispatch)) {
                $dispatch = self::routeCheck($request, $config);
            }

            // 記錄當前調度信息
            $request->dispatch($dispatch);


......
    $data = self::exec($dispatch, $config);//這個函數很關鍵

繼續跟進routeCheck這個函數,同樣在App.php裏面

kDo5K1.png

繼續跟進到path方法裏面,然後這裏有一個pathinfo()函數,繼續跟進

public function path()
{
    if (is_null($this->path)) {
        $suffix   = Config::get('url_html_suffix');
        $pathinfo = $this->pathinfo();
        if (false === $suffix) {
            // 禁止僞靜態訪問
            $this->path = $pathinfo;
        } elseif ($suffix) {
            // 去除正常的URL後綴
            $this->path = preg_replace('/\.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo);
        } else {
            // 允許任何後綴訪問
            $this->path = preg_replace('/\.' . $this->ext() . '$/i', '', $pathinfo);
        }
    }
    return $this->path;
}

Config::get('var_pathinfo')是配置文件中的設置的參數,默認值爲s,怎麼找到這個變量?可以全局搜索一下,可以搜索到其中一個配置文件裏面有

kDThFS.png

從GET中獲取鍵值,然後賦值給routeCheck中的$path,這裏也就是index/think\app/invokefunction

kDTvYF.png

然後開始進入路由檢測的部分,經過check的檢查後會進入else的分支,但這一部分對於我們需要控制的變量沒有任何影響,關鍵是$result以及$must這兩個變量的賦值結果,這也是導致了後面操作的關鍵,可以進入Route::parseUrl函數

public static function routeCheck($request, array $config)
    {
        $path   = $request->path();
        $depr   = $config['pathinfo_depr'];
        $result = false;

        // 路由檢測
        $check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on'];
        if ($check) {
            // 開啓路由
            if (is_file(RUNTIME_PATH . 'route.php')) {
                // 讀取路由緩存
                $rules = include RUNTIME_PATH . 'route.php';
                is_array($rules) && Route::rules($rules);
            } else {
                $files = $config['route_config_file'];
                foreach ($files as $file) {
                    if (is_file(CONF_PATH . $file . CONF_EXT)) {
                        // 導入路由配置
                        $rules = include CONF_PATH . $file . CONF_EXT;
                        is_array($rules) && Route::import($rules);
                    }
                }
            }

            // 路由檢測(根據路由定義返回不同的URL調度)
            $result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
            $must   = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must'];

            if ($must && false === $result) {
                // 路由無效
                throw new RouteNotFoundException();
            }
        }

        // 路由無效 解析模塊/控制器/操作/參數... 支持控制器自動搜索
        if (false === $result) {
            $result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
        }

        return $result;
    }

跟進Route::parseUrl函數

public static function parseUrl($url, $depr = '/', $autoSearch = false)
    {

        if (isset(self::$bind['module'])) {
            $bind = str_replace('/', $depr, self::$bind['module']);
            // 如果有模塊/控制器綁定
            $url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr);
        }
        $url              = str_replace($depr, '|', $url);
        list($path, $var) = self::parseUrlPath($url);
        ....
         return ['type' => 'module', 'module' => $route];
    }

再跟進一下parseUrlPath(),這裏面就是返回一個$path變量,對包含模塊/控制器/操作的URL進行分割成數組進行返回

kDHQgJ.png

回到上一層的函數中,繼續跟進,可以發現在自動搜索控制器的判斷中進入了else語句,從而爲控制器進行了賦值,這裏是個賦值點,很關鍵

kDHtUK.png

然後以$route變量返回上層run函數

kDHrDI.png

此時dispatchself::exec()dispatch 進入到self::exec()中,繼續跟進。此時的dispatch 裏面是一個以module爲名字的數組,所以進入exec函數中必將進入分支爲module的模塊,然後進入self::module函數

kDbesA.png

protected static function exec($dispatch, $config)
    {
        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;
            ...............
        }

        return $data;
    }

跟進self::module函數,在進入多模塊部署後由於,bind的值爲null,會進入elseif的條件,使available的變量成爲true,這也是後面爲什麼可以順利初始化module的條件,不然就會拋出異常XD。

kDbqeI.png

    public static function module($result, $config, $convert = null)
    {
        if (is_string($result)) {
            $result = explode('/', $result);
        }

        $request = Request::instance();

        if ($config['app_multi_module']) {
            // 多模塊部署
            $module    = strip_tags(strtolower($result[0] ?: $config['default_module']));
            $bind      = Route::getBind('module');
            $available = false;

            if ($bind) {
                // 綁定模塊
                list($bindModule) = explode('/', $bind);

                if (empty($result[0])) {
                    $module    = $bindModule;
                    $available = true;
                } elseif ($module == $bindModule) {
                    $available = true;
                }
            } elseif (!in_array($module, $config['deny_module_list']) && is_dir(APP_PATH . $module)) {
                $available = true;
            }

            // 模塊初始化
            if ($module && $available) {
                // 初始化模塊
                $request->module($module);
                $config = self::init($module);

                // 模塊請求緩存檢查
                $request->cache(
                    $config['request_cache'],
                    $config['request_cache_expire'],
                    $config['request_cache_except']
                );
            } else {
                throw new HttpException(404, 'module not exists:' . $module);
            }
        } else {
            // 單一模塊部署
            $module = '';
            $request->module($module);
        }

        // 設置默認過濾機制
        $request->filter($config['default_filter']);

        // 當前模塊路徑
        App::$modulePath = APP_PATH . ($module ? $module . DS : '');

        // 是否自動轉換控制器和操作名
        $convert = is_bool($convert) ? $convert : $config['url_convert'];

        // 獲取控制器名
        $controller = strip_tags($result[1] ?: $config['default_controller']);
        $controller = $convert ? strtolower($controller) : $controller;

        // 獲取操作名
        $actionName = strip_tags($result[2] ?: $config['default_action']);
        if (!empty($config['action_convert'])) {
            $actionName = Loader::parseName($actionName, 1);
        } else {
            $actionName = $convert ? strtolower($actionName) : $actionName;
        }

        // 設置當前請求的控制器、操作
        $request->controller(Loader::parseName($controller, 1))->action($actionName);

        // 監聽module_init
        Hook::listen('module_init', $request);

        try {
            $instance = Loader::controller(
                $controller,
                $config['url_controller_layer'],
                $config['controller_suffix'],
                $config['empty_controller']
            );
        } catch (ClassNotFoundException $e) {
            throw new HttpException(404, 'controller not exists:' . $e->getClass());
        }

        // 獲取當前操作名
        $action = $actionName . $config['action_suffix'];

        $vars = [];
        if (is_callable([$instance, $action])) {
            // 執行操作方法
            $call = [$instance, $action];
            // 嚴格獲取當前操作方法名
            $reflect    = new \ReflectionMethod($instance, $action);
            $methodName = $reflect->getName();
            $suffix     = $config['action_suffix'];
            $actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName;
            $request->action($actionName);

        } elseif (is_callable([$instance, '_empty'])) {
            // 空操作
            $call = [$instance, '_empty'];
            $vars = [$actionName];
        } else {
            // 操作不存在
            throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()');
        }

        Hook::listen('action_begin', $call);

        return self::invokeMethod($call, $vars);
    }

繼續跟進的就是我在文章一開頭說的內容,controller變量就被賦值,然後獲得方法名字,開始請求這個方法

kDqoNV.png

最後還是返回了這個方法

return self::invokeMethod($call, $vars);

$call變量是個數組,裏面包含了控制器以及操作,可以追蹤裏面的變量變化

kDL3uj.png

然後通過ReflectionMethod方法去構造一個映射,反正就把他當成平常一個類去調用某個方法,接着就把剩餘的url的剩餘內容賦值給args,最後調用invokefunction函數,這個函數也類似回調函數,所以就會把&function=call_user_func_array&vars[0]=system&vars[1][]=dir傳進invokefunction這個方法裏面。

kDLOG8.png

kDOPI0.png

可以看到裏面args裏面的內容結構,裏面包含了多個數組

kDO2es.png

繼續跟進的話,你會發現這個函數跟上面跟進的函數的套路一模一樣,也是利用了回調的效果,也是利用一個變量把system後面的內容返回給call_user_func_array,只不過這次可以直接調用call_user_func_array了,相當於call_user_func_array("system","dir")

kDvUeJ.png

補丁後的效果

再來觀察一下加上補丁的走向,直接就會進入拋出異常的步驟,只要匹配到不是字母開頭的控制器的話直接進入異常,有效避免利用命名空間構造攻擊鏈

kDx8AA.png

小結

  1. 這個攻擊鏈的構造,還是概念模糊,如果真正構造的時候需要怎麼去做?這裏只是根據別人的payload去分析代碼,分析它的攻擊過程,個人感覺真正核心的東西沒掌握,也有可能看得多就會了???XD因爲這個東西不只是這個模塊可以如此調用,還有其他模塊也有同樣的效果,這也比較考驗對該框架的熟悉程度,多接觸開發還是很好的。
  2. 看了好幾篇文章,發現這幾個都是差不多從路由的檢測開始跟進,其實想想也對,畢竟payload從url中來,跟進某函數跟到底了再返回,有可能這是一種套路,先記下來。。。
  3. 我也是第一次審計這種東西,畢竟ThinkPHP 5.0.x 的代碼執行漏洞,從漏洞技術含量和利用鏈構造上來看,算是2018年一個很牛的洞了,對我這種菜雞,學習到就好。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章