type
status
date
slug
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
2.2 安装 Laravel
.env.example
默认配置是debug
模式,可不予理会然后启动 Apache 环境即可,访问
这里不用
php artisan serve
启动因为没有Apache集成环境debug方便![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fb637a930-b710-4ad5-81f2-8a6c0a226123%2FUntitled.png?table=block&id=a3ce9e0d-bb42-473d-b72c-b7528a60d90e&t=a3ce9e0d-bb42-473d-b72c-b7528a60d90e&width=2316&cache=v2)
2.3 编写漏洞入口代码
先在
resources/views
目录下新建一个文件 hello.blade.php
,内容为routes/web.php
增加路由![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fa111a0ac-50d8-4dfe-81f1-531b3e70c283%2FUntitled.png?table=block&id=d7eb2960-891b-4493-a7e9-6e2512fe24de&t=d7eb2960-891b-4493-a7e9-6e2512fe24de&width=1574&cache=v2)
访问这个视图
正常出现变量未定义报错信息
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F9cf4b23b-da29-435c-8c83-6c902060c731%2FUntitled.png?table=block&id=729f0d4c-77f0-4a37-b86f-9912f086ebae&t=729f0d4c-77f0-4a37-b86f-9912f086ebae&width=2228&cache=v2)
3. 漏洞分析
3.1 触发点路由追踪
点击
Make variable optional
并抓包,这个方法会自动把我们模板的 {{ $username }}
替换为 {{ $username ?? '' }}
,即添加一个默认值![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F745cad22-5612-4509-bae6-a83d80c45435%2FUntitled.png?table=block&id=03b82569-54e5-4aea-9dc6-9efb90262bd2&t=03b82569-54e5-4aea-9dc6-9efb90262bd2&width=1974&cache=v2)
可以看到,请求的路径为
_ignition/execute-solution
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F528278b0-bdf9-4392-936a-d62d07b9a943%2FUntitled.png?table=block&id=5a8a535a-19dc-4cc0-ae79-a7225f29c3d2&t=5a8a535a-19dc-4cc0-ae79-a7225f29c3d2&width=1298&cache=v2)
全局分别搜一下
_ignition
和 execute-solution
可知本API对应的方法为
vendor/facade/ignition/src/IgnitionServiceProvider.php
中的 ExecuteSolutionController
类![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fbc555f6a-deec-4856-be8a-3206903fca99%2FUntitled.png?table=block&id=a8427cb4-896d-44bb-a398-92975a2a460e&t=a8427cb4-896d-44bb-a398-92975a2a460e&width=1648&cache=v2)
跟进此类,下个断点发现
ExecuteSolutionController
类对象会被当做函数调用![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F65681cc9-18ea-4277-be00-e7aaa7b5aa4f%2FUntitled.png?table=block&id=7b22a9ae-22b0-43cd-9deb-4ac5afde5cb0&t=7b22a9ae-22b0-43cd-9deb-4ac5afde5cb0&width=1850&cache=v2)
与其定义的
__invoke
魔术方法一致。在
__invoke
魔术方法中先调用获取
$solution
对象随后调用 $solution
对象的 run
方法跟进得知
$solution
即为我们 POST 传入的跟进
$solution->run();
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fac3df5e5-d5e9-4a99-ac00-8625b3cbf196%2FUntitled.png?table=block&id=90274291-91db-43e8-b8bc-c69f9734cdcd&t=90274291-91db-43e8-b8bc-c69f9734cdcd&width=3202&cache=v2)
逻辑可简化为
其中
$parameters
为我们 POST 传入的之所以可以简化为两行,是因为中间的逻辑主要为,在
$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](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F8ee01d79-6550-4831-8ddf-29e7f31ec1c3%2FUntitled.png?table=block&id=6c50c7c7-67f4-4777-af99-9b31c126bf7e&t=6c50c7c7-67f4-4777-af99-9b31c126bf7e&width=2934&cache=v2)
格式可分为 4 部分,时间、错误、错误描述、错误堆栈
先尝试一下输入的内容是否能正常的写入日志文件中
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F94d87cc1-b121-4357-bc86-72ec5de7b758%2FUntitled.png?table=block&id=f02ca86b-7e07-433d-a21d-403ec6538687&t=f02ca86b-7e07-433d-a21d-403ec6538687&width=2646&cache=v2)
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fd6213650-229b-4fe2-9e09-c2cb8a37be1d%2FUntitled.png?table=block&id=2c691089-570d-4bc0-a3b4-53fd894064ce&t=2c691089-570d-4bc0-a3b4-53fd894064ce&width=3482&cache=v2)
可以正常写入,也就是,日志文件大概如下结构,其中payload为我们可控部分
问题在于,如何在存在
[prefix]
[midfix1]
[midfix2]
[suffix]
的情况下,如何让可控PAYLOAD
被利用?首先,考虑
phar://
数据流包装器,因为 file_get_contents
会触发 phar 的反序列化,通过 viewFile 传入即可。但这里会遇到几个问题3.3 尝试 base64 过滤器修改内容
PHP 中
base64
解码会过滤除特殊字符,如![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F625aeeb8-4454-4286-b5b8-e14a07a2c910%2FUntitled.png?table=block&id=47c368c6-c910-4b42-a241-ed8b41fa694b&t=47c368c6-c910-4b42-a241-ed8b41fa694b&width=784&cache=v2)
'1'
等价于 base64_decode('MQ==')
等价于 base64_decode('[;#MQ==#...<<>')
然后PHP的
base64_decode
解码还有一个特性,在需要解码的字符串长度不足4的倍数时,会自动添加 =
号填充,也就是说,没有因输入的字符串不符合base64编码后的规则而抛出异常一说,直至把字符串解码为空串![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F8689fd64-7d5e-4b9b-bfcd-9b092ee58165%2FUntitled.png?table=block&id=b9a83633-c254-4453-853f-6fec32d7c2af&t=b9a83633-c254-4453-853f-6fec32d7c2af&width=1658&cache=v2)
但问题在于,这个值难以控制,如日志里的时间
稍微把5变为6,结果则不尽相同。此外,后面还有调用栈等复杂的字符串,处理起来比较麻烦。
不过PHP base64特性也不是一无是处,我们可以利用它清空
laravel.log
使我们受到的干扰进一步降低。经实验,258278字符的日志文件在经过 8次 base64_decode
后即可变为空。即多请求几次这个接口即可清空日志文件。理想中是美好的,但意外却发生了
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F65daf97e-e332-40cd-9c54-170914541a4d%2FUntitled.png?table=block&id=96295dea-1f79-4700-a5b8-867a13b62a4f&t=96295dea-1f79-4700-a5b8-867a13b62a4f&width=2648&cache=v2)
上图中报错,是因为日志文件中含有
=
号,但等号在 base64编码中是结束用的填充符号,会使得PHP base64解码过滤器抛出异常[5]。原来,filter过滤器中的base64解码和直接用
base64_decode
是不同的。3.3小节开头中用base64_decode
函数字符串中含有 =
号是能正常解码的。但假设不存在
=
号,清空文件是无问题的哈![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F434e7961-64dc-4fa0-b839-f8efe5bc435d%2FUntitled.png?table=block&id=f96041ea-6160-44e8-ae5e-3670afbb8e11&t=f96041ea-6160-44e8-ae5e-3670afbb8e11&width=2672&cache=v2)
或者一步到位,结合多个base64解码过滤器
最终发现base64过滤器各种直接用不上,但问题不大,这里记得他的一个特性,如果不存在
=
号,他可以帮助我们去掉非base64编码后的字符。3.4 考虑其他 filter 过滤器
清空文件除了使用 base64-decode过滤器,也可以使用,而且不会受到特殊符号的影响
Emm,不过就是不知道这个
filter
是如何发现的。。网上找了半天没找到至此,上面的问题还是没有解决
问题在于,如何在存在
[prefix]
[midfix1]
[midfix2]
[suffix]
的情况下,如何让可控PAYLOAD
被利用?来硬的不行,换个思路,假设可以让
[prefix]
[midfix1]
[midfix2]
[suffix]
转换为不可见字符,保留 PAYLOAD
部分为可见字符,然后结合PHP base64 解码的特性,是不是就可以只保留 PAYLOAD
部分了?好像还真可以,利用
filter过滤器
把日志内容识别为 UTF-16
编码,并转换为 UTF-8
编码,因为 UTF-16
是双字节编码,那么原有的 [prefix]
是否就全变为不可见字符?然后
PAYLOAD
部分,输入 P\0A\0Y\0L\0O\0A\0D\0
,写入时指定 utf16le -> utf-8转换为 UTF-8 刚刚好为
PAYLAOD
,最后在 base64解码一下,刚刚好只剩下 PAYLOAD
部分。赶紧试试,太骚了这思路。![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F5713ffbe-7837-4cf9-a10f-e2b7b2b4ef56%2FUntitled.png?table=block&id=31001e0b-5a99-4807-a2c5-435394ba08c4&t=31001e0b-5a99-4807-a2c5-435394ba08c4&width=2402&cache=v2)
发现写不进去。。仔细想想,
\0
在PHP是阶段,会报错,也难怪,而且到时候 file_get_contets() 的时候,也会报错。这里又需利用到另外一个过滤器
我们可以先把
\0
编码为 =00
,读取的时候解码即可。先写入PAYLOAD,这里为
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Febb33f7b-4284-4fe2-8265-3e2a789f71da%2FUntitled.png?table=block&id=581aa7c7-2f1a-4559-b1b1-c895a2366b8d&t=581aa7c7-2f1a-4559-b1b1-c895a2366b8d&width=2654&cache=v2)
然后写入,先解码
=00
然后在把utf16
转换为utf8
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fbb2414a5-dc14-4f2f-a7c2-e2a65059a05b%2FUntitled.png?table=block&id=d46eb62c-5f09-4ba0-8306-2f4be62bd9a7&t=d46eb62c-5f09-4ba0-8306-2f4be62bd9a7&width=2664&cache=v2)
又报错了,utf16要求双字节对齐,但日志文件来说,不一定是双字节对齐。处理方法很简单,任何数*2都是偶数,就双字节对齐啦,如变成下面这样
赶紧尝试一下 (
不出意外,又报错了
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Ff7548c1f-cbdc-452e-b946-b9ab9943aa2e%2FUntitled.png?table=block&id=4470a5ca-f913-45b9-83db-076c2d8cbff8&t=4470a5ca-f913-45b9-83db-076c2d8cbff8&width=2474&cache=v2)
找了半天,原来
PAYLOAD
被部分截断了,然后截断的位置非常神奇,并且加了个点 =0.
,导致解码错误,![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F471db460-2a58-4595-99c8-c5a8934e5b37%2FUntitled.png?table=block&id=b3de305f-ac9c-4db8-85f1-c8661ce3b7d3&t=b3de305f-ac9c-4db8-85f1-c8661ce3b7d3&width=2584&cache=v2)
写个小demo,果然如此
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fdeb7911f-f091-4120-b21a-e178149f5512%2FUntitled.png?table=block&id=d3f02324-5105-4f9c-9db7-e77c0701ab90&t=d3f02324-5105-4f9c-9db7-e77c0701ab90&width=1688&cache=v2)
即我们的
部分PAYLOAD
导致出了问题这个容易解决,查看日志文件,数一下发现只会截断前15个,我们这里填充个100个随意字符,这里选择填充
A
,怎么也不会殃及到我们的PAYLOAD
也就是说,我们现在不用关心
部分PAYLOAD
了,现变为先清空日志,然后发两次
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fa7db7043-0ed4-42fc-805f-f3a1b048b858%2FUntitled.png?table=block&id=2bf06eed-ca7d-429c-a759-8f6ba2d54308&t=2bf06eed-ca7d-429c-a759-8f6ba2d54308&width=2906&cache=v2)
转换
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F85e86004-c2fb-4a43-9a84-e0ad42b491e3%2FUntitled.png?table=block&id=5cf2a948-e1bc-4183-8f00-3dfd293f27cc&t=5cf2a948-e1bc-4183-8f00-3dfd293f27cc&width=2838&cache=v2)
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fc483c3ea-0112-4643-93c1-4b560a53e27b%2FUntitled.png?table=block&id=276d1ec2-045a-44c7-bd3b-7daf7decbe91&t=276d1ec2-045a-44c7-bd3b-7daf7decbe91&width=958&cache=v2)
nice~
特殊字符用base64编码去特殊字符就行,当然我们的
tari
没有进行base64编码,所以直接base64解码,特殊字符是去掉了,不过我们的 tari
也会被解码为乱码看到了熟悉的有问题的子串
==00
号,就和我们日志里出现的 =0.
类似,都会有问题这里有两个方法
- 编码
=
,即编码为=3D
- base64编码后,把
=
号去掉,因为PHP的base64解码发现位数不为4的整数倍,会自动在后方加=
号,这点在3.3小节说过
这里选择第2种方法
3.5 两个PAYLOAD和双字节对齐
但是这样还有个问题,就是我们的
PAYLOAD
出现了两次,处理方法很简单,把原来的转换为
其中
PAYLOAD_1
随意即可,无需为真的PAYLOAD
,比如我们可以把 PAYLOAD_1
变为保持为100个 'A'
,因为没有 \0
,所以从utf16
转换为utf-8
会把他转换为”乱码”到这里可能会有所疑问,我们的
PAYLOAD_2
不是也出现了两次么?这里也挺巧妙的,一开始我们是清空了日志的,这时我们的日志文件
laravel.log
文件为空。PAYLOAD_1
和 PAYLOAD_2
前面都填充了 100个 A
,所以 [midfix1][midfix2][suffix]
部分是一样的,而且是不会变的。![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F683e9684-9dfa-436e-bafa-ff02cb5c8ef8%2FUntitled.png?table=block&id=9797b5b6-67fc-4edf-bad6-12db0b333c5a&t=9797b5b6-67fc-4edf-bad6-12db0b333c5a&width=1694&cache=v2)
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fa4923c9e-c54e-4be2-bb10-63ac263c7037%2FUntitled.png?table=block&id=2c9be323-611c-4b69-86f7-6ab8b257ad53&t=2c9be323-611c-4b69-86f7-6ab8b257ad53&width=1686&cache=v2)
首先看看整体是不是双字节对齐的
==证明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.ERRORmidfix1
长度为 119,为奇数设定 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]
和 [midfix1]
都为奇数, 且 PAYLOAD_1
为长度偶数,最终可以安全的得到一个 PAYLOAD_2
- 另外一种情形
因为包的
[prefix]
和 [midfix1]
部分刚好都为奇数,所以是这样。如果是其他情况呢,假设
[prefix]
和 [midfix1]
是前奇后偶的情况?这个和实际利用情况很像,因为如果file_get_contents 输入的参数过长,
[midfix1]
会变成长度是 110
就需要在我们想要的PAYLOAD最后填充
=00
,就可以了,因为这样无论第1个或第2个PAYLOAD前面是奇数还是偶数,首先双字节一定是对齐的,然后第1个PAYLOAD前是奇数,会吃掉第1个PAYLOAD,又因PAYLOAD后有一个=00
会吃掉后面一个字符,这样就变成了偶数,所以第二个不会被吃,反则反之。所以前奇后偶的情况是这样
因为第1个payload前只有奇数或偶数两种情况,所以考虑这两种情况足以。
那么针对这两种情况,有没有通用的解决方法呢?(我暂没发现。。
3.6 整合
- 清空日志
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F18e9458e-cfaa-4462-aaa9-900528c75ab1%2FUntitled.png?table=block&id=30064ce2-02b8-47f1-a9bc-4fbc0d2115f7&t=30064ce2-02b8-47f1-a9bc-4fbc0d2115f7&width=2412&cache=v2)
- 写入填充数据
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F8734026a-8b3c-4351-9956-1f948db1a189%2FUntitled.png?table=block&id=25c862f7-7b28-4fae-8c86-764c9bf983da&t=25c862f7-7b28-4fae-8c86-764c9bf983da&width=720&cache=v2)
- 写入我们可控数据
这里Payload字段比较短,为双奇数型,不需要处理
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F2c1cc237-dd22-4d49-bfdd-7287efc5a9cb%2FUntitled.png?table=block&id=6d9f202c-f492-422b-a5d6-0fe4af9ad744&t=6d9f202c-f492-422b-a5d6-0fe4af9ad744&width=2588&cache=v2)
如果比较长,比如真实漏洞利用,一般为前奇后偶型,需要在
PAYLOAD
后缀加上 =00
保证对齐
4. 转换去除无关数据
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F91ae6568-9bdd-42a5-9b84-735dc94803ff%2FUntitled.png?table=block&id=cbfc6b49-c37a-4b73-b2f1-1c74e3fe7047&t=cbfc6b49-c37a-4b73-b2f1-1c74e3fe7047&width=2410&cache=v2)
OK,终于可控了~
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F7af7386d-94be-42b1-bacb-585041c2729a%2FUntitled.png?table=block&id=9bb44f4a-0902-43b2-8555-49bafd81742d&t=9bb44f4a-0902-43b2-8555-49bafd81742d&width=756&cache=v2)
4. 漏洞利用
4.1 手工利用
4.1.1 清空日志
4.1.2 写入填充对齐数据
4.1.3 生成并写入 phar payload
4.1.3.1 生成phar payload
4.1.3.2 写入phar payload 数据 (4.1.3.1 生成的)
因为payload比较长,所以是前奇后偶型,注意别忘了在最后还要补上一个
=00
这里已经补上了4.1.4 把日志文件转换为仅剩phar数据
4.1.5 通过file_get_contents触发phar反序列化
注意这里的路由必须为绝对路径(相对路径试过了会报错)
不过绝对路径也是已知的,可以通过报错信息获取~
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Ff61bd290-aee6-4a0a-b597-fea3731845b2%2FUntitled.png?table=block&id=5b62f035-7f5d-4013-bbdd-7bbeb203922c&t=5b62f035-7f5d-4013-bbdd-7bbeb203922c&width=2586&cache=v2)
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等)做了巩固和更深入的理解和应用。