| 项目 | 内容 |
|---|---|
| 平台题名 | L杯-misc-01 编码层层 |
| 分类 / 难度 | Misc / Encoding,Medium(实际难度 Easy-Medium) |
| 入口 | http://<host>/ |
| 附件 | 页面上的「下载附件」→ challenge.txt(GET /?download=1) |
| 提交物 | flag{...} |
重要提示:本题附件一个环境只能下载一次。 下载成功后服务会立刻退出(
start.sh轮询/tmp/downloaded后exit 0)。所以先把文件保存到本地再看,不要只是浏览器里点开预览一下就关掉——那样文件不会落到磁盘,而题目环境已经关掉了。
一、拿到附件
curl -s -o challenge.txt "http://<host>/?download=1"
file challenge.txt
wc -c challenge.txt
内容长这样:
YQHL_ARCHIVE
--------------------
This file contains an archived message.
The message has been transformed multiple times.
WWd4aG1kMmh0ZHo4WVRJd01XWm1XV1p0WlRkNVl6QTJNREkyT1dSbE9XWm1ZVEptTXpWbU5tSTJPR1ppTWpJM1ptSTFPV1k9PQ==
--------------------
[ops] Node registration for this host is still pending. Complete it
before the next review window:
curl -s "https://scheduler.internal.yqhl.cloud/register"
Registration state: /internal/node-status/
有用的只有中间那长长的一行。题目描述里已经给了提示:The message has been transformed multiple times.——多次变换,也就是多层编码。
二、判断编码类型
中间那行的字符集是 A-Z a-z 0-9 + / =,结尾带 == 填充——这是标准 Base64。长度是 4 的倍数,很规整。
题干里提到了「多次」,我们要做的是从外往里一层层剥。每剥一层,观察输出是什么字符集,再决定下一步用什么解码器。
三、逐层剥
第 1 层:Base64
base64 -d <<< 'WWd4aG1kMmh0ZHo4WVRJd01XWm1XV1p0WlRkNVl6QTJNREkyT1dSbE9XWm1ZVEptTXpWbU5tSTJPR1ppTWpJM1ptSTFPV1k9PQ=='
输出:
Wgxhmd2htdz8YTIwMWZmWWZtZTd5YzA2MDI2OWRlOWZmYTJmMzVmNmI2OGZiMjI3ZmI1OWY9PQ==
还是一串 Base64 风格的字符,但长度变短了,且全部落在 A-Za-z0-9 范围内。继续。
第 2 层:还是 Base64
base64 -d <<< 'Wgxhmd2htdz8YTIwMWZmWWZtZTd5YzA2MDI2OWRlOWZmYTJmMzVmNmI2OGZiMjI3ZmI1OWY9PQ=='
输出:
5c61d3c8a10ff878...(一段纯 0-9 / a-f 的串)
这一层出来的全是十六进制字符——Hex。注意它是偶数长度,可以直接 xxd -r -p 还原为字节。
第 3 层:Hex
xxd -r -p <<< '<hex 串>'
输出又是一串 Base64(例如 ZmxhZ3tzaW1wbGVfbGF5ZXJfY29kaW5nX2Zvcl9taXNjfQ==)。这层最容易被忽略:Hex 解出来不一定是明文,也可能还是 Base64。
第 4 层:Base64
base64 -d <<< '<base64 串>'
输出:
gur...(一串英文字母)
到这里字符仍然是英文字母,但一眼能看出单词形状歪了——这是 ROT13 的典型特征(flag 变成 synt,the 变成 gur)。
第 5 层:ROT13
tr 'A-Za-z' 'N-ZA-Mn-za-m' <<< '<rot13 串>'
得到最终明文:
flag{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}
四、一条命令走完(Python)
import base64, codecs, re, urllib.request
raw = open("challenge.txt", "r", encoding="utf-8").read()
# 取出正文里最长的那一行(跳过 YQHL_ARCHIVE 头和 [ops] 尾注)
line = max((l.strip() for l in raw.splitlines()), key=len)
data = line
data = base64.b64decode(data) # 1: Base64
data = base64.b64decode(data) # 2: Base64
data = bytes.fromhex(data.decode()) # 3: Hex
data = base64.b64decode(data).decode() # 4: Base64
flag = codecs.decode(data, "rot_13") # 5: ROT13
print(flag)
也可以完全用命令行串起来(调试时更直观,每步都能看到中间结果):
L=$(grep -v '^-' challenge.txt | grep -v '^\[ops\]' | grep -v '^ ' | grep . | tail -1)
echo "$L" | base64 -d | base64 -d | xxd -r -p | base64 -d | tr 'A-Za-z' 'N-ZA-Mn-za-m'
五、解码链总览
challenge.txt
│ Base64
▼ Base64
│ Hex
▼ Base64
│ ROT13
▼
flag{...}
从产物倒推,编码顺序是:
flag → ROT13 → Base64 → Hex → Base64 → challenge.txt
解码必须逆序,这也是所有多层编码题的通则:编码是套娃,解码是拆娃。
六、易错点
- 只解一层就以为完事。Base64 解出来还是 Base64 的情况很常见(因为 Base64 的字符集恰好也是
A-Za-z0-9+/),一定要看输出字符集再决定下一步。 challenge.txt里不只有密文。开头的YQHL_ARCHIVE/ 分隔线 / 末尾的[ops]运维备注都不是密文,直接丢给解码器会报错。用max(..., key=len)取最长行,或者手工挑那一行。- Hex 长度必须为偶数。如果
xxd -r -p报错或者少一位,说明你复制时丢了字符(终端换行最常见)。 - ROT13 容易被误判为「已经解完了」。看到全是英文字母但读不通,先试 ROT13 和它的两个变体(ROT47 也行)——ROT13 是自逆的,
tr两遍就还原。 - 本题是一次性附件。第一次
curl -o没存到文件(比如用了curl -s ... | head),环境就直接下线了;这种情况下只能重新起一个实例(平台题目页「重建环境」)。
七、知识点与思路沉淀
- ROT13 / Base64 / Hex 都不是加密,是编码。它们不提供任何机密性——本题的设计目的就是让人建立「编码 ≠ 加密」的直觉。
- 识别靠字符集:
A-Za-z0-9+/=→ Base64;纯0-9a-f且偶数长 → Hex;-_→ URL-safe Base64;全是字母但不成词 → ROT13。 file/head -c/wc -c是 Misc 的标准三件套,拿到任何附件先跑一遍,比直接上工具更快。- 真实场景里多层编码常出现在恶意脚本(PowerShell 混淆、宏病毒)里,本类题的解法链路和日常取证是完全一致的。
八、真实环境验证
一次性容器实测(附件已通过 /tmp/downloaded 触发一次性下线,属正常设计):
$ curl -s -o challenge.txt "http://<host>/?download=1"
$ python3 solve.py
flag{misc01_local_ok}
实际比赛环境里 flag 由平台注入,值不同、格式一致。