漏洞概述
漏洞编号:CVE-2021-3129
影响版本 :
Laravel 框架 < 8.4.3
facade ignition 组件 < 2.5.2
当Laravel开启了Debug模式时,由于Laravel自带的Ignition组件对file_get_contents()和file_put_contents()函数的不安全使用,攻击者可以通过发起恶意请求,构造恶意Log文件等方式触发Phar反序列化,最终造成远程代码执行。
关于Ignition 组件可以参考 : https://segmentfault.com/a/1190000020377961
可以在Laravel框架出错时提供优美的报错界面和报错信息,并对一些问题可以提供一些解决方案或者第三方解决方案 (后面搭建环境就用到了这个功能)
环境搭建
- PHP : 7.3.4
- Laravel : v8.44.0
1 | git clone https://github.com/laravel/laravel.git |
然后把源码中的 .env.example
改成 .env
, 框架从 .env
加载环境变量后才能正常运行,否则打开链接后是error
然后打开框架链接,点击Generate app key生成加密密钥, 这个密钥值在生产后可以在.env
文件中找到
环境搭建over.
漏洞分析
问题具体出现在ignition的默认Solutions, 具体在vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php
调用任意soluction
把.env
里面的app_key
删除,然后再次调用用ignition的解决方案,并抓包看一下如何调用的GenerateAppKeySolution
在GenerateAppKeySolution的run方法下断点, 观察调用堆栈可以向上找到ExecuteSolutionController
然后在ExecuteSolutionController重新下断点跟踪
进入getRunnableSolution()
再进入$this->getSoluction()
发现会根据传的solution参数值实例化任意类,这样就可以控制调用任意soluction,
参数是通过$request->get('parameters', [])
获得, 这样参数也可控
MakeViewVariableOptionalSolution
参数设置成如下值,调用MakeViewVariableOptionalSolution
1 | {"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"viewFile": "test"}} |
然后进入solution的run方法
发现存在file_get_contents操作, 并且无过滤. 所以这里可以利用phar://协议触发反序列化达到RCE目的.
如果后期利用框架进行开发的人员,写出了一个文件上传的功能。那么我们就可以上传一个恶意phar文件,利用上述的file_get_contents()
去触发phar反序列化,达到rce的效果。这种利用方式依赖laravel框架的二次开发.
如先用phpggc生成一个phar文件,手动到放到框架目录里
1 | php -d'phar.readonly=0' ./phpggc monolog/rce1 call_user_func phpinfo --phar phar -o 1.gif |
1 | { |
利用log反序列化
log清除
原理是file_get_contents支持php://filter
参数和php://filter
的一些特性
convert.base64-decode
过滤器会将一些非base64字符给过滤掉后再进行decode
, 所以可以通过调用多次convert.base64-decode
多次触发该特性来将log清空
具体分析在: https://xz.aliyun.com/t/9030#toc-7
清空日志payload, 执行之后日志会被清除
1 | { |
不带variableName参数会有warning,所以就加了一个参数
写日志
先生成payload:
1 | php -d'phar.readonly=0' ./phpggc monolog/rce1 system dir --phar phar -o php://output | base64 -w0 |
1 | import base64 |
这个payload经过一系列解码后可以变成一个正常的phar文件
echo file_get_contents('php://filter/read=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=payload. txt');
下面是写入日志并恢复为phar文件再利用的步骤:
日志格式:
1 | [时间] [某些字符] PAYLOAD [某些字符] PAYLOAD [某些字符] 部分PAYLOAD [某些字符] |
发送下面的包:
1 | POST /_ignition/execute-solution |
日志中会有两个完整的PAYLOADPAYLOADPAYLOADPAYLOAD
和一个部分的PAYLOADPAYLOADP...
日志解码
先分别按照上面三个过滤器的步骤解码:
原文件应该是这样的:
第一次解码后:
convert.quoted-printable-decode
解析=xx
这样的字符,这一步也是有坑的,只是这里没遇到, 如果第三个不完整的payload 是
=
或者=x
就会出错,这一步刚好是=xx
详细内容参考: https://blog.csdn.net/rfrder/article/details/117427130
- 第二步解码:
convert.iconv.utf-16le.utf-8
这一步有两个坑,
- 必须保证日志全部字符内容为偶数个,(
convert.iconv.utf-16le.utf-8
是把两个字符变成一个字符 )
否则会报错
1 | iconv stream filter (&quot;utf-16le&quot;=&gt;&quot;utf-8&quot;): invalid multibyte sequence |
- payload前面是偶数个字符,否则解码得到的是乱码而不是base64
这里遇到的是偶数个字符,但是payload前面是奇数个.可以在payload前面多加一个=xx
, 保证payload可以被正常解码.因为有两个payload,所以payload加一个字符不会影响全部字符奇偶数
这一步可以在payload多加一个字符来把第二个payload吃掉,最后只有一个payload会成功转换base64格式,
另外会遇到日志字符个数为奇数个的情况. 可以参考https://blog.csdn.net/rfrder/article/details/117427130, 再发送一个不带payload的请求.
- base64解码, 这一步没什么坑
convert.base64-decode
- phar反序列化
phar://../storage/logs/laravel.log
参考链接
https://xz.aliyun.com/t/9030#toc-7