漏洞介绍

Thinkphp5.x版本(5.0.20)中没有对路由中的控制器进行严格过滤,在存在admin、index模块、没有开启强制路由的条件下(默认不开启),导致可以注入恶意代码利用反射类调用命名空间其他任意内置类,完成远程代码执行。


影响版本

THINKPHP 5.0.5-5.0.22

THINKPHP 5.1.0-5.1.30


漏洞分析

这里选择对5.0.20版本进行分析,关键函数开头

/thinkphp/library/think/App.php:120

 $dispatch = self::$dispatch;
            // 未设置调度信息则进行 URL 路由检测
            if (empty($dispatch)) {
                $dispatch = self::routeCheck($request, $config);
            }
            // 记录当前调度信息
            $request->dispatch($dispatch);
            // 记录路由和请求信息
            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');
            }
            // 监听 app_begin
            Hook::listen('app_begin', $dispatch);
            // 请求缓存检查
            $request->cache(
                $config['request_cache'],
                $config['request_cache_expire'],
                $config['request_cache_except']
            );
            $data = self::exec($dispatch, $config);


进入self::routeCheck函数,进行路由检查

/thinkphp/library/think/App.php:120

 public static function routeCheck($request, array $config)
    {
        $path = $request->path();
        ........................
    }


进入$request->path()函数

/thinkphp/library/think/Request.php:416行

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;
    }


进入$this->pathinfo()函数

/thinkphp/library/think/Request.php:384行

    public function pathinfo()
    {
        if (is_null($this->pathinfo)) {
            if (isset($_GET[Config::get('var_pathinfo')])) {
                // 判断URL里面是否有兼容模式参数
                $_SERVER['PATH_INFO'] = $_GET[Config::get('var_pathinfo')];
                unset($_GET[Config::get('var_pathinfo')]);
            } elseif (IS_CLI) {
                // CLI模式下 index.php module/controller/action/params/...
                $_SERVER['PATH_INFO'] = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : '';
            }
            // 分析PATHINFO信息
            if (!isset($_SERVER['PATH_INFO'])) {
                foreach (Config::get('pathinfo_fetch') as $type) {
                    if (!empty($_SERVER[$type])) {
                        $_SERVER['PATH_INFO'] = (0 === strpos($_SERVER[$type], $_SERVER['SCRIPT_NAME'])) ?
                        substr($_SERVER[$type], strlen($_SERVER['SCRIPT_NAME'])) : $_SERVER[$type];
                        break;
                    }
                }
            }
            $this->pathinfo = empty($_SERVER['PATH_INFO']) ? '/' : ltrim($_SERVER['PATH_INFO'], '/');
        }
        return $this->pathinfo;
    }

Config::get('var_pathinfo')是配置文件中的设置的参数,默认值为s,从GET中获取键值,然后赋值给routeCheck中的$path,这里也就是index/think\app/invokefunction。

image-20181212125055499.png


/thinkphp/library/think/App.php:120

 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;
    }

这里会开始进行路由检测,检查$check后会进入else分支导入路由配置,然后接着检测路由表url调度结果$result,如果调度失败并且开启了强制路由$must,就抛出异常,这就是漏洞利用条件之一,接着进入了Route::parseUrl函数,根据$path(我们可控的url)进行模块/控制器解析。

进入Route::parseUrl函数

/thinkphp/library/think/Route.php:1204行

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


进入self::parseUrlPath函数

/thinkphp/library/think/Route.php

 private static function parseUrlPath($url)
    {
        // 分隔符替换 确保路由定义使用统一的分隔符
        $url = str_replace('|', '/', $url);
        $url = trim($url, '/');
        $var = [];
        if (false !== strpos($url, '?')) {
            // [模块/控制器/操作?]参数1=值1&参数2=值2...
            $info = parse_url($url);
            $path = explode('/', $info['path']);
            parse_str($info['query'], $var);
        } elseif (strpos($url, '/')) {
            // [模块/控制器/操作]
            $path = explode('/', $url);
        } else {
            $path = [$url];
        }
        return [$path, $var];
    }

对包含模块/控制器/操作的URL进行分割成数组进行返回

image-20181212165908049.png

list($path, $var) = self::parseUrlPath($url);
        $route            = [null, null, null];
        if (isset($path)) {
            // 解析模块
            $module = Config::get('app_multi_module') ? array_shift($path) : null;
            if ($autoSearch) {
                // 自动搜索控制器
                $dir    = APP_PATH . ($module ? $module . DS : '') . Config::get('url_controller_layer');
                $suffix = App::$suffix || Config::get('controller_suffix') ? ucfirst(Config::get('url_controller_layer')) : '';
                $item   = [];
                $find   = false;
                foreach ($path as $val) {
                    $item[] = $val;
                    $file   = $dir . DS . str_replace('.', DS, $val) . $suffix . EXT;
                    $file   = pathinfo($file, PATHINFO_DIRNAME) . DS . Loader::parseName(pathinfo($file, PATHINFO_FILENAME), 1) . EXT;
                    if (is_file($file)) {
                        $find = true;
                        break;
                    } else {
                        $dir .= DS . Loader::parseName($val);
                    }
                }
                if ($find) {
                    $controller = implode('.', $item);
                    $path       = array_slice($path, count($item));
                } else {
                    $controller = array_shift($path);
                }
            } else {
                // 解析控制器
                $controller = !empty($path) ? array_shift($path) : null;
            }
            // 解析操作
            $action = !empty($path) ? array_shift($path) : null;
            // 解析额外参数
            self::parseUrlParams(empty($path) ? '' : implode('|', $path));
            // 封装路由
            $route = [$module, $controller, $action];
            // 检查地址是否被定义过路由
            $name  = strtolower($module . '/' . Loader::parseName($controller, 1) . '/' . $action);
            $name2 = '';
            if (empty($module) || isset($bind) && $module == $bind) {
                $name2 = strtolower(Loader::parseName($controller, 1) . '/' . $action);
            }
            if (isset(self::$rules['name'][$name]) || isset(self::$rules['name'][$name2])) {
                throw new HttpException(404, 'invalid request:' . str_replace('|', $depr, $url));
            }
        }
        return ['type' => 'module', 'module' => $route

再将返回结果赋值$path,提取路由信息,再封装到$route,最后再返回。

image-20181212172910488.png

thinkphp/library/think/App.php:120行

          if (empty($dispatch)) {
                $dispatch = self::routeCheck($request, $config);
            }
            // 记录当前调度信息
            $request->dispatch($dispatch);
            // 记录路由和请求信息
            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');
            }
            // 监听 app_begin
            Hook::listen('app_begin', $dispatch);
            // 请求缓存检查
            $request->cache(
                $config['request_cache'],
                $config['request_cache_expire'],
                $config['request_cache_except']
            );
            $data = self::exec($dispatch, $config);

进入self::exec函数

thinkphp/library/think/App.php:453行

   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;
        .........................  
    }


进入module函数

thinkphp/library/think/App.php:493行

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;

根据$config['app_multi_module']进入多模块绑定分支,$bind为NULL,又进入elseif分支,判断模块是否在禁止的列表里面$config['deny_module_list'],并且模块目录存在,$available = true,就不会抛出异常,这也就是为什么Payload当中的模块需要存在的条件。

一路跟到module函数最后的返回值,$controller没有进行过滤,此时为think\app,也就是return self::invokeMethod($call, $vars);


进入self::invokeMethod函数

thinkphp/library/think/App.php:329行

public static function invokeMethod($method, $vars = [])
{
    if (is_array($method)) {
        $class = is_object($method[0]) ? $method[0] : self::invokeClass($method[0]);
        $reflect = new \ReflectionMethod($class, $method[1]);
    } else {
        // 静态方法
        $reflect = new \ReflectionMethod($method);
    }
    $args = self::bindParams($reflect, $vars);
    self::$debug && Log::record('[ RUN ] ' . $reflect->class . '->' . $reflect->name . '[ ' . $reflect->getFileName() . ' ]', 'info');
    return $reflect->invokeArgs(isset($class) ? $class : null, $args);
}

image-20181212191031058.png

此时穿进去的$call也就是$method是一个数组,第一个元素是一个think\App对象,第二个元素则是调用方法名称的字符串invokefunction,然后通过反射ReflectionMethod获取这个对象下对应的方法。

再通过$args = self::bindParams($reflect, $vars);获取传入的参数,也就是Payload

image-20181212191850592.png

最后在调用反射$reflect->invokeArgs($args);,将Payload数组传入反射对象函数invokeFunction,完成代码执行。

public static function invokeFunction($function, $vars = [])
    {
        $reflect = new \ReflectionFunction($function);
        $args = self::bindParams($reflect, $vars);
        // 记录执行信息
        self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info');
        return $reflect->invokeArgs($args);
    }


源链接

Hacking more

...