thinkphp

ThinkPHP是一个免费开源的,快速、简单的面向对象的轻量级PHP开发框架,是为了敏捷WEB应用开发和简化企业应用开发而诞生的。ThinkPHP从诞生以来一直秉承简洁实用的设计原则,在保持出色的性能和至简的代码的同时,也注重易用性。遵循Apache2开源许可协议发布,意味着你可以免费使用ThinkPHP,甚至允许把你基于ThinkPHP开发的应用开源或商业产品发布/销售。

漏洞分析

thinkphp/library/think/Request.php:504 Request类的method方法
image.png
可以通过POST数组传入__method改变this->{this->method}($_POST);达到任意调用此类中的方法。

然后我们再来看这个类中的__contruct方法

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中,可以覆盖类属性,那么我们可以通过覆盖Request类的属性
image.png
这样filter就被赋值为system()了,在哪调用的呢?我们要追踪下thinkphp的运行流程 thinkphp是单程序入口,入口在public/index.php,在index.php中

require DIR . '/../thinkphp/start.php';
引入框架的start.php,跟进之后调用了App类的静态run()方法

image.pngimage

看下run()方法的定义

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

    // 记录路由和请求信息
    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');
    }
    ...省略...
        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':
                $data = $dispatch['response'];
                break;
            default:
                throw new \InvalidArgumentException('dispatch type not support');
        }
}

首先是经过dispatch = self::routeCheck(request, config)检查调用的路由,然后会根据debug开关来选择是否执行Request::instance()->param(),然后是一个switch语句,当dispatch等于controller或者method时会执行Request::instance()->param(),只要是存在的路由就可以进入这两个case分支。

而在 ThinkPHP5 完整版中,定义了验证码类的路由地址?s=captcha,默认这个方法就能使$dispatch=method从而进入Request::instance()->param()。

我们继续跟进Request::instance()->param()
image.png
执行合并参数判断请求类型之后return了一个input()方法
image.png
将被__contruct覆盖掉的filter字段回调进filterValue(),这个方法我们需要特别关注了,因为 Request 类中的 param、route、get、post、put、delete、patch、request、session、server、env、cookie、input 方法均调用了 filterValue 方法,而该方法中就存在可利用的 call_user_func 函数
image.png
call_user_func调用system造成rce。

梳理一下:this->method可控导致可以调用__contruct()覆盖Request类的filter字段,然后App::run()执行判断debug来决定是否执行request->param(),并且还有dispatch['type'] 等于controller或者 method 时也会执行request->param(),而$request->param()会进入到input()方法,在这个方法中将被覆盖的filter回调call_user_func(),造成rce。
image.png

实战

xray扫c段的时候扫到一个thinkphp的rce
image.png
放到burpsuite里面发包
image.png
挂马
image.png
蚁剑连接
image.png