结合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时就会形成这样的语句 event可控,传入数组键为system键值为id时就会形成这样的语句response=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方法中, 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->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也就是起始类中传入的 command也就是起始类中传入的this->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。