| 项目 | 内容 |
|---|---|
| 平台题名 | L杯-misc-02 损坏的归档 |
| 分类 / 难度 | Misc / File Forensics,Medium |
| 入口 | http://<host>/ |
| 附件 | evidence.jpg(GET /?download=1) |
| 提交物 | flag{...} |
同样是一次性附件:下载成功后服务立刻退出。务必
curl -o存到本地再分析。
一、拿到附件并做初次判断
curl -s -o evidence.jpg "http://<host>/?download=1"
ls -l evidence.jpg
file evidence.jpg
file 会说它是 JPEG image data ... 1x1。但文件大小明显不是一张 1×1 图片该有的体积(1×1 的 JPEG 通常几百字节,这里明显更大)。
直接打开图片看,只有一个极小的方块(1×1 像素),没有任何内容——题目名字叫「损坏的归档」,描述说「This file survived more than one format conversion」。
二、找文件末尾
JPEG 的结束标记固定是 FF D9。查看文件末尾:
tail -c 64 evidence.jpg | xxd
你会看到类似:
00000000: ... ffd9 504b 0304 1400 0000 0000 ...
^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^
EOI PK\x03\x04 ...
FF D9(JPEG EOI)后面紧跟 50 4b 03 04(ZIP local file header 的魔数 PK\x03\x04)。这就是全部答案:
这个文件是一张完整 JPEG + 一个完整 ZIP 首尾拼接。
用 binwalk 也能一步看出来:
binwalk evidence.jpg
# DECIMAL HEXADECIMAL DESCRIPTION
# 0 0x0 JPEG image data
# 404 0x194 Zip archive data ...
本题的偏移是固定的:JPEG 段长 404 字节,FF D9 位于 402(最后两个字节),ZIP 头 PK\x03\x04 位于 404,两者首尾相接、中间不隔任何字节。
三、拆出 ZIP
方法 A:直接让 unzip 干(大多数环境都行)
unzip evidence.jpg -d out/
unzip / 7z / binwalk -e 都是扫描 EOCD(End of Central Directory)来定位 ZIP 的,所以对「前面粘了别的东西」的文件天然容忍。
方法 B:按偏移 dd 出来(最稳妥,也最能讲清楚原理)
# 偏移 = "PK\x03\x04" 出现的位置
OFF=$(grep -abo $'PK\x03\x04' evidence.jpg | head -1 | cut -d: -f1)
echo "ZIP 起始偏移: $OFF"
dd if=evidence.jpg of=hidden.zip bs=1 skip=$OFF status=none
unzip -l hidden.zip
unzip -l 会列出三个文件:
Length Date Time Name
--------- ---------- ----- ----
187 1980-01-01 00:00 README.txt
220 1980-01-01 00:00 OPS-NOTES.txt
57 1980-01-01 00:00 archive.dat
(这里用的是「存储」模式 compression=0,所以长度就是原样长度,肉眼可读。)
四、读三个文件
cd out
cat README.txt
This file survived more than one format conversion.
If you found something after the image ended,
maybe the image was not the only thing stored here.
The last layer is not encryption.
关键提示是最后一句:The last layer is not encryption. —— 最后一层不是加密,那么它只能是编码。
OPS-NOTES.txt 又是那条熟悉的「节点注册未完成,去 curl 某个 URL」的运维备注。和本题目标没有因果关系,直接跳过,不要去请求它。
archive.dat 才是目标:
cat archive.dat
# 5a6d78685a33747461584e6a4d444a666247396a5957786662327439
全部由 0-9a-f 组成,偶数长度——标准 Hex。
五、解最后一层
# 1) Hex → Base64
xxd -r -p archive.dat
# 2) Base64 → 明文
xxd -r -p archive.dat | base64 -d
得到:
flag{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}
一条命令:
xxd -r -p out/archive.dat | base64 -d
Python 版:
import base64
data = open("out/archive.dat", "rb").read().strip()
flag = base64.b64decode(bytes.fromhex(data.decode())).decode()
print(flag)
六、整体结构图
evidence.jpg
│
├── JPEG 数据(404 字节,完整、合法、可渲染)
│ …
│ FF D9 ← JPEG EOI,偏移 402
│
└── ZIP(偏移 404) ← PK\x03\x04
│
├── README.txt ← 提示:"最后一层不是加密"
├── OPS-NOTES.txt ← 与本题无关的运维备注(忽略)
└── archive.dat
│
└── Hex( Base64( flag ) )
七、易错点
- 以为图片「打不开」就是文件坏了。其实 JPEG 部分是完好的,只是 1×1 太小看着像空白。判断文件结构要看
tail -c/binwalk,不要凭肉眼。 - 忘了
FF D9只是 JPEG 的结束,不是文件的结束。这是「文件拼接(polyglot)」类题的核心:一个文件可以同时是两种格式。 - 只解 Hex 就停了。
xxd -r -p出来是ZmxhZ3...这种明显的 Base64,必须再解一层。 - 被
OPS-NOTES.txt带偏。这个文件是环境的运维噪声,和题目链路无因果关系——看到「请执行 curl」这种催促性文本,先问一句「它和我要找的 flag 有因果吗」,没有就跳过。 unzip报 warning 很正常。因为 ZIP 前面有额外数据,unzip可能提示extra bytes at beginning,加-q或忽略即可,解出来的文件是完整的。
八、知识点与思路沉淀
file只识别文件头,不保证整个文件都是那种格式。binwalk/tail -c/ 搜魔数才是判断「文件里还有什么」的正确方法。- 常见魔数速查:
FF D8 FFJPEG 开始 /FF D9JPEG 结束 /PK\x03\x04ZIP local header /PK\x05\x06ZIP EOCD /%PDF/\x1f\x8bgzip /7z\xbc\xaf/Rar!。 - ZIP 的定位靠 EOCD 而不是起始偏移,所以 ZIP 几乎可以被追加到任何文件后面而不损坏——这正是「图种」「polyglot」手法的基础。
- Hex→Base64 这种「编码套编码」在恶意样本里非常常见(十六进制打包再 Base64 传输),本题是它的无害化教学版。
九、真实环境验证
一次性容器实测:
$ curl -s -o evidence.jpg "http://<host>/?download=1"
$ python3 -c "d=open('evidence.jpg','rb').read();print(d.find(b'\xff\xd9'), d.find(b'PK\x03\x04'))"
402 404
$ # dd 出 ZIP、解压、解 Hex、解 Base64
flag{misc02_local_ok}
实际比赛环境里 flag 由平台注入,值不同、格式一致。