结合phpggc学习lavaral反序列化pop链

结合phpggc学习lavaral反序列化pop链

phpgcc的几条链子

Laravel/RCE1 5.4.27 rce __destruct
Laravel/RCE2 5.5.39 rce __destruct
Laravel/RCE3 5.5.39 rce __destruct *
Laravel/RCE4 5.5.39 rce __destruct
Laravel/RCE5 5.8.30 rce __destruct *
Laravel/RCE6 5.5.* rce __destruct *
Laravel/RCE7 ? <= 8.16.1 rce __destruct *

以上是phpggc中利用的几条利用链,其中这七条利用链起始点都一样,都是Illuminate\BroadcastingPendingBroadcast的__destruct方法引起的。

RCE1

出发点位于vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php

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

很自然想到__call方法,全局搜索存在__call方法的类,这里用的是Faker\Gererator,部分代码实现如下

......
public function __call($method, $attributes)
{
    return $this->format($method, $attributes);
}
......
public function format($formatter, $arguments = array())
{
    return call_user_func_array($this->getFormatter($formatter), $arguments);
}
......
public function getFormatter($formatter)
{
    if (isset($this->formatters[$formatter])) {
        return $this->formatters[$formatter];
    }
    foreach ($this->providers as $provider) {
        if (method_exists($provider, $formatter)) {
            $this->formatters[$formatter] = array($provider, $formatter);


            return $this->formatters[$formatter];
        }
    }
    throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}

存在可利用的call_user_func_array方法,并且其参数都是我们可控的实现反序列化

RCE2

利用链的入口还是和第一条链一样,不同的是没有找__call方法而是直接进入dispatch方法,然后找到某个类中存在可利用的dispatch方法,其中该方法的参数$event又是我们可控的,这里找到Dispatcher类,函数实现如下

public function dispatch($event, $payload = [], $halt = false)
{
    [$event, $payload] = $this->parseEventAndPayload(
        $event, $payload
    );


    if ($this->shouldBroadcast($payload)) {
        $this->broadcastEvent($payload[0]);
    }


    $responses = [];


    foreach ($this->getListeners($event) as $listener) {
        $response = $listener($event, $payload);

        if ($halt && ! is_null($response)) {
            return $response;
        }

        if ($response === false) {
            break;
        }


        $responses[] = $response;
    }

    return $halt ? null : $responses;
}

受前面的影响我直接去找call_user_func和call_user_func_array,但是没有找到相关的利用链,重新看下参数的传递,传入$event,这里着重关注getListenners方法,跟进

public function getListeners($eventName)
{
    $listeners = $this->listeners[$eventName] ?? [];


    $listeners = array_merge(
        $listeners,
        $this->wildcardsCache[$eventName] ?? $this->getWildcardListeners($eventName)
    );


    return class_exists($eventName, false)
                ? $this->addInterfaceListeners($eventName, $listeners)
                : $listeners;
}

首先这里的return肯定是返回$listeners,因为并不是类。
然后再回到这两行代码

foreach ($this->getListeners($event) as $listener) {
    $response = $listener($event, $payload);

e v e n t 可 控 , 传 入 数 组 键 为 s y s t e m 键 值 为 i d 时 就 会 形 成 这 样 的 语 句 event可控,传入数组键为system键值为id时就会形成这样的语句 eventsystemidresponse=system(‘id’,[]);保存返回$response造成rce

RCE3

同样还是相同的出发点,还是找__call方法,找到类Illuminate\Notifications\ChannelManager,该类继承manager类,其中有__call方法(在挖掘的时候找到__call方法后不仅要看该类是否存在可利用的方法,其子类也要看),其实现如下

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方法实现在子类,如下

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

d e f a u l t C h a n n e l 的 值 是 我 们 可 控 的 , 比 如 是 ′ n u l l ′ , 然 后 继 续 回 到 d r i v e r 方 法 中 , defaultChannel的值是我们可控的,比如是'null',然后继续回到driver方法中, defaultChannelnulldriverthis->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->app);
}

其中参数我们都可控造成rce

RCE4

一样的起点->找_call方法->Illuminate\Validation\Validator.php->callExtension方法中call_user_func_array函数的利用造成rce

和第一条链一样的思路不一样的链而已,不再赘述

RCE5

起始点还是一样,思路也差不多,找到可利用类存在dispatch方法,然后调用任意类的任意方法。这里找到src/Illuminate/Bus/Dispatcher.php,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,第2个参数是将传入的$command实例的connection属性值赋值给$connection,但是需要if ($this->queueResolver && $this->commandShouldBeQueued($command))语句成立


$this->queueResolver && $this->commandShouldBeQueued($command)

跟进commandShouldBeQueued方法

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

说明 c o m m a n d 也 就 是 起 始 类 中 传 入 的 command也就是起始类中传入的 commandthis->event需要是ShouldQueue的实例,查看实现其接口的类

lavarel5.6 5.5
在这里插入图片描述

lavarel5.8 5.7
在这里插入图片描述

我们找到src/Illuminate/Broadcasting/BroadcastEvent.php,其部分实现如下

public function __construct($event)
{
    $this->event = $event;
}


public function handle(Broadcaster $broadcaster)
{
    $name = method_exists($this->event, 'broadcastAs')
            ? $this->event->broadcastAs() : get_class($this->event);


    $broadcaster->broadcast(
        Arr::wrap($this->event->broadcastOn()), $name,
        $this->getPayloadFromEvent($this->event)
    );
}

其中$ event是我们可控的。即传入$connection也是可控的。

首先分析第一个参数,找到一个可以实现RCE的函数,其中EvalLoader中存在eval函数,跟进

class EvalLoader implements Loader
{
    public function load(MockDefinition $definition)
    {
        if (class_exists($definition->getClassName(), false)) {
            return;
        }


        eval("?>" . $definition->getCode());
    }
}

$this->queueResolver = [new \Mockery\Loader\EvalLoader(), ‘load’];(这里需要通过无参数的构造方法传入我们想要利用的函数)

然后来看 q u e u e = c a l l u s e r f u n c ( queue = call_user_func( queue=calluserfunc(this->queueResolver, $connection);中的第二个参数

刚刚找到了可利用的类方法load,load方法里的参数必须是MockDefinition的实例,即$connection必须是MockDefinition的实例,看到MockDefinition

class MockDefinition
{
    protected $config;
    protected $code;


    public function __construct(MockConfiguration $config, $code)
    {
        if (!$config->getName()) {
            throw new \InvalidArgumentException("MockConfiguration must contain a  name");
        }
        $this->config = $config;
        $this->code = $code;
    }


    public function getConfig()
    {
        return $this->config;
    }


    public function getClassName()
    {
        return $this->config->getName();
    }


    public function getCode()
    {
        return $this->code;
    }
}

eval执行的参数getcode()返回值即code变量我们可控, BroadcastEvent但是构造函数中还有一条件即if (! c o n f i g − > g e t N a m e ( ) ) , 这 里 我 们 使 config->getName()),这里我们使 config>getName())使config为不存在的类就欧克

追溯一下其参数值

起始点$ this->events->dispatch($ this->event)

【$ this->event可控,值为BroadcastEvent($ evilCode)】
因为dispatcher.php中【要求传入该类的参数必须为ShouldQueue实例,找到其实现类BroadcastEvent,$this->event可控】

【$ this->connection可控,值为MockDefinition($ evilCode)】
因为要实现eval函数的参数可控就要传入MockDefinition实例,调用MockDefinition类

最终MockDefinition->getcode()传入eval的参数实现rce

RCE6

这条链和rce5如出一辙,刚开始我找了半天不知道为什么要利用MessageBag类,因为该类有__tostring方法,我就觉得是利用某个字符串操作函数到这个类里面,之后对比了一下payload
在这里插入图片描述
在这里插入图片描述

事实上这里返回的是一样的只是后者经过该类处理了一下,查看文档大概是说处理错误消息的功能。

RCE7

这里利用链起始点是一样的,但是较高版本中/Illuminate/Bus/Dispatcher.php实现是这样的

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

之前利用的类是BroadcastEvent类,且很多版本都能使用这里没有一一本地测试,但是ShouldQueue实现类在之后更新中又新增了一个类,也就是CallQueuedClosure类,这里同样也是两个参数可控,和上面大同小异。

环境搭建

这里随便选择一个版本,我这里选的是lavarel5.6.21版本,github上下载来后本地composer install,phpstudy一键搭好环境。

5.5.40及之前版本和5.6.x版本至5.6.29版本都存在反序列化漏洞
影响版本:5.5.x<=5.5.40、5.6.x<=5.6.29

漏洞分析

一是利用之前公开的pop链,二是利用app_key的泄露以及XSRF-TOKEN参数可控实现rce

这里先来看看XSRF-TOKEN的利用链,直接定位到关键类

App\Http\Middleware\EncryptCookies 和 App\Http\Middleware\VerifyCsrfToken
加解密方法是一样的所以这里拿第一个来说,追随到App\Http\Middleware\EncryptCookies类中,部分实现如下:

......

public function __construct(EncrypterContract $encrypter)
{
    $this->encrypter = $encrypter;
}
......
public function handle($request, Closure $next)
{
    return $this->encrypt($next($this->decrypt($request)));
}

其构造方法传入$encrypter对象,handle方法处理获取的cookie,其中调用decrypt方法,跟进

protected function decrypt(Request $request)
{
    foreach ($request->cookies as $key => $cookie) {
        if ($this->isDisabled($key)) {
            continue;
        }


        try {
            $request->cookies->set($key, $this->decryptCookie($key, $cookie));
        } catch (DecryptException $e) {
            $request->cookies->set($key, null);
        }
    }


    return $request;
}

这里不用很明显是遍历cookies然后将其解密,继续 跟进

protected function decryptCookie($name, $cookie)
{
    return is_array($cookie)
                    ? $this->decryptArray($cookie)
                    : $this->encrypter->decrypt($cookie, static::serialized($name));
}

最终$cookie不是数组时会调用encrypter接口中的decrypt方法,最终返回,具体实现如下

public function decrypt($payload, $unserialize = true)
{
    $payload = $this->getJsonPayload($payload);


    $iv = base64_decode($payload['iv']);


    // Here we will decrypt the value. If we are able to successfully decrypt it
    // we will then unserialize it and return it out to the caller. If we are
    // unable to decrypt this value we will throw out an exception message.
    $decrypted = \openssl_decrypt(
        $payload['value'], $this->cipher, $this->key, 0, $iv
    );


    if ($decrypted === false) {
        throw new DecryptException('Could not decrypt the data.');
    }


    return $unserialize ? unserialize($decrypted) : $decrypted;
}

很明显最后会返回一个反序列化值,对cookie进行解码之后调用openssl_decrypt函数解密,并且当key泄露时cookie是完全可控的,配合脚本生成payload实现RCE。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值