GHCTF题目研究(超大坑)

复现

ez_readfile

这题的exp执行时会吞字符,导致验证不通过,在这点卡了好久,结束时看WP出题人直接把check_vulnerable检查函数的failure换成pass跳过验证,而吞字符具体的原因他也不知道,好吧那就自己研究

1
2
3
4
5
6
7
8
9
10
<?php
show_source(__FILE__);
if (md5($_POST['a']) === md5($_POST['b'])) {
if ($_POST['a'] != $_POST['b']) {
if (is_string($_POST['a']) && is_string($_POST['b'])) {
echo file_get_contents($_GET['file']);
}
}
}
?>

这题的思路是先用强碰撞绕过,再利用CVE-2024-2961漏洞读取文件

1
2
3
4
5
6
def send(self, path: str) -> Response:
"""Sends given `path` to the HTTP server. Returns the response.
"""
data='a=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2&b=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2'
headers={"Content-Type": "application/x-www-form-urlencoded"}
return self.session.post(self.url, params={"file": path},data=data,headers=headers)

再send函数中配置发送请求的参数(返回file_get_contents所需的条件),例如这题中需要a跟b的md5相等,get参数为file,值要传入file_get_contents,由程序生成(path)

1
2
3
4
5
6
7
8
9
10
def download(self, path: str) -> bytes:
"""Returns the contents of a remote file.
"""
print(path)
path = f"php://filter/convert.base64-encode/resource={path}"
response = self.send(path)
## 回显提取
data = response.re.search(b'</code>(.*)', flags=re.S).group(1)
print(response.text)
return base64.decode(data)

download函数提取回显内容

然而在检查发送与相应的数据时出现了问题

1
2
3
4
5
6
[-] Remote.download did not return the test string
--------------------
Expected test string: b'IocjTMU03JWKjFdaYvtre7whxt9hX6tlrFgpme8XTcuDOmdJq1'
Got: b'IocjTMU03JWKjFdaYvtre7whxt9hX6tlrFgpme8XTcuDOmdJ'
--------------------
[-] If your code works fine, it means that the data:// wrapper does not work

发送的字符串比收到的字符串多两位

我的payload是php://filter/convert.base64-encode/resource=data:text/plain;base64,SW9jalRNVTAzSldLakZkYVl2dHJlN3doeHQ5aFg2dGxyRmdwbWU4WFRjdURPbWRKcTE=

手动执行返回SW9jalRNVTAzSldLakZkYVl2dHJlN3doeHQ5aFg2dGxyRmdwbWU4WFRjdURPbWRK

base64解码后是IocjTMU03JWKjFdaYvtre7whxt9hX6tlrFgpme8XTcuDOmdJ,确实为Got的内容

而payloadphp://filter/convert.base64-encode/resource=data:text/plain;base64,SW9jalRNVTAzSldLakZkYVl2dHJlN3doeHQ5aFg2dGxyRmdwbWU4WFRjdURPbWRKcTE=,通过base64解码后是IocjTMU03JWKjFdaYvtre7whxt9hX6tlrFgpme8XTcuDOmdJq1,为Expected的内容,由此可以看出并不是脚本的问题,更有可能是php本身的问题导致吞字符

于是我准备本地复现下

1
[*] The data:// wrapper works

有意思的来了,检查通过了

我本地php版本是7.1,而题目的版本是7.3,遂切换版本

1
2
3
4
5
6
[-] Remote.download did not return the test string
--------------------
Expected test string: b'ZFbUbqgX02ZZabrFnyYxMpqAQJeNt1o07KGuXewgRhuZyYGos1'
Got: b'ZFbUbqgX02ZZabrFnyYxMpqAQJeNt1o07KGuXewgRhuZyYGo'
--------------------
[-] If your code works fine, it means that the data:// wrapper does not work

果然,php7.3中会出现这个问题(在7.0、7.1、7.4下均未出现此问题)

执行php://filter/convert.base64-encode/resource=data:text/plain,test会返回dGVz而不是dGVzdA==,会删除4个字符,就是解码后的2个字符

test -> dGVz correct:dGVzdA==

testt -> dGVz correct dGVzdHQ=

testtt -> dGVzdHR0 correct dGVzdHR0

由此可见,当加密后的base64不满足4的整数倍时(没=补位)会直接舍弃不满足的位数

这么明显的bug网络上应该有所提及吧,于是搜索了好久,终于在github的issues中搜索convert.base64-encode关键字找到了这个漏洞的成因

github:https://github.com/php/php-src/pull/1153 phpbug提交:https://bugs.php.net/bug.php?id=68532

原来早在2014年就发现了这个漏洞

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
Description:
------------
When using a memory stream and the read filter "convert.base64-encode" the last character is missing from the output if the base64 conversion needs padding bytes.
This doesn't happen when using a file.

Test script:
---------------
$testString = 'test';
$stream = fopen('php://memory','r+');
fwrite($stream, $testString);
rewind($stream);
$filter = stream_filter_append($stream, 'convert.base64-encode');
echo "memoryStream = " . stream_get_contents($stream).PHP_EOL;


$fileStream = fopen(__DIR__.'/base64test.txt','w+');
fwrite($fileStream , $testString);
rewind($fileStream );
$filter = stream_filter_append($fileStream , 'convert.base64-encode');
echo "fileStream = " . stream_get_contents($fileStream ).PHP_EOL;


Expected result:
----------------
memoryStream = dGVzdA==
fileStream = dGVzdA==


Actual result:
--------------
memoryStream = dGVz
fileStream = dGVzdA==

(话说怎么跟我一样都用test测试)

之后我在php5.3中发现了同样的bug,在php5.6就修复了此bug,兜兜转转到了php7.3这个bug居然又出现了

phpbug:https://bugs.php.net/bug.php?id=75910

最新的报告在php7.2.2中,提到了这个bug在旧版本中存在,评论中指出会影响到php7.3

1
2
3
4
It seems that this bug is not limited to macOS, and it's affecting PHP 7.2.0 up to the latest 7.3 (which is 7.3.3 RN).
Proof: https://3v4l.org/NnFV2

笔者:https://3v4l.org 能同时运行超过300种php版本,能快速找到不同版本运行的差异

ps:php的版本管理这么混乱吗

结语

至此,确定了在某些php版本(5.3、7.3…)在使用convert.base64-encode时由于过滤器的缺陷,流过滤器在处理分块数据时未正确缓存和补全末尾字节,且未在数据流结束时强制添加必要的=填充符,使得convert.base64-encode解密的结果出现缺失

回过头来看exp脚本,只要限制发送的测试位数为4的整数倍就能不触发此bug