一次从 Git 暂存区找回 flag 的过程
在文件管理器里看不到一个文件,是否意味着它真的不存在了?
在普通目录中,答案可能是“是”;但在 Git 仓库里,工作区只是其中一层。文件即使已经从工作区删除,它的内容仍可能保存在暂存区、历史提交、其他分支,甚至尚未被清理的 Git 对象中。
这次遇到的 CTF 题目,就是利用了 Git 的这一特点。题目目录表面上只剩一个 .git 文件夹,看起来没有任何可以分析的文件,但真正的 flag 仍保存在 Git 暂存区中。
题目描述
题目给出的提示大意是:
有人使用 Git 藏了一个 flag,你能找到它吗?
打开题目目录后,没有看到普通文件,目录里只剩下:
.git/
这意味着题目重点并不在当前可见文件,而在 Git 保存的仓库信息中。
Git 为什么还能保存被删除的文件
要理解这道题,首先需要区分 Git 中的三个区域:
| 区域 | 保存的内容 |
|---|---|
| 工作区 | 当前在文件夹中看到和编辑的文件 |
| 暂存区 | 执行git add 后,准备放进下一次提交的文件快照 |
| 本地仓库 | 已经通过git commit 保存的历史版本 |
很多人第一次学习 Git 时,会把 git add 理解成“告诉 Git 新增了一个文件”。更准确地说,git add 是把文件当前的内容快照写入暂存区。
因此,完全可能出现这样的状态:
flag.txt的某个版本已被加入暂存区;- 工作区中的
flag.txt随后被删除; - 文件管理器中已经看不到它;
- 暂存区里仍然保存着删除前的内容。
这正是这道题使用的隐藏方式。
第一步:确认它是不是 Git 仓库
先进入题目目录,查看隐藏文件:
ls -la
Windows PowerShell 可以使用:
Get-ChildItem -Force
如果能看到 .git,说明该目录包含完整或部分 Git 仓库数据。接下来不要急着恢复文件,更不要执行可能覆盖现场的命令,先读取仓库状态:
git status
也可以使用更紧凑的输出:
git status --short
本题的关键状态类似:
MD flag.txt
第二步:理解 MD flag.txt
git status --short 输出的前两列分别代表:
XY 文件名
X:暂存区相对于当前提交的状态;Y:工作区相对于暂存区的状态。
因此:
MD flag.txt
可以拆解成:
M:暂存区中保存了一个修改后的flag.txt;D:工作区中的flag.txt已被删除。
这条状态已经给出了非常明确的线索:文件只是从工作区消失了,暂存区中仍有一份内容。
第三步:直接读取暂存区中的文件
Git 可以通过下面的语法读取暂存区中的文件:
git show :flag.txt
这里文件名前面的冒号非常重要:
:flag.txt
它表示读取 Git 索引,也就是暂存区中的 flag.txt,而不是读取工作区文件。
执行后得到了类似下面的提示:
flag is me,use base64 and put in moectf{}:ZzE3XzE1X3NvXzNhU3k=
由此可以判断:
- 冒号后面是一段 Base64;
- 解码后的内容需要放进
moectf{}; - 这才是题目的真实答案,而不是仓库中其他历史对象里的诱导内容。
第四步:解码 Base64
Linux 中可以这样解码:
echo 'ZzE3XzE1X3NvXzNhU3k=' | base64 -d
PowerShell 可以使用:
[Text.Encoding]::UTF8.GetString(
[Convert]::FromBase64String('ZzE3XzE1X3NvXzNhU3k=')
)
解码结果为:
g17_15_so_3aSy
按照文件中的提示套上 flag 格式,最终得到:
moectf{g17_15_so_3aSy}
为什么没有直接去翻提交历史
看到题目提到 Git,常见的第一反应是执行:
git log --all --oneline --graph
git reflog
git branch -a
git stash list
git fsck --full
这些检查方向没有错,本题仓库中也确实存在悬空提交和 blob,看起来很像 flag 的藏身之处。但如果一开始就逐个检查所有对象,不仅效率较低,也容易被诱导信息带偏。
更合理的顺序是先查看:
git status
git status --short
因为 git status 描述的是仓库当前最直接的异常。在这道题中,MD flag.txt 已经明确说明暂存区与工作区之间存在差异,没有必要先进行更复杂的对象恢复。
这次解题过程中虽然也检查到了被 reset 的提交和悬空对象,但最终通过暂存区中的内容进行交叉验证,才排除了烟雾弹。
git show :flag.txt 背后的原理
Git 的暂存区通常由 .git/index 保存。它不是普通意义上的文件夹,而是一份索引,记录下一次提交准备使用的文件信息及对应的 Git 对象。
当执行:
git add flag.txt
Git 会把 flag.txt 的内容保存为 blob 对象,并让暂存区指向这个对象。
之后即使工作区文件被删除,只要暂存区没有被重新覆盖,对应的 blob 内容通常仍然可以通过索引读取。命令:
git show :flag.txt
本质上就是根据暂存区记录找到相应对象,再输出它的内容。
还可以使用下面的命令查看暂存区记录的文件:
git ls-files --stage
输出通常包含文件模式、对象哈希、暂存阶段和文件名:
100644 <blob-hash> 0 flag.txt
如果想直接读取这个 blob,也可以执行:
git cat-file -p <blob-hash>
不过在本题中,git show :flag.txt 已经是最简单直接的做法。
Git 类 CTF 的推荐排查顺序
以后遇到类似题目,可以按照从简单到复杂的顺序排查。
1. 查看当前状态
git status
git status --short
git diff
git diff --cached
分别检查工作区、暂存区和当前提交之间的差异。
2. 检查暂存区
git ls-files --stage
git show :文件名
特别关注已在工作区删除、但暂存区仍保留的文件。
3. 检查提交历史
git log --all --decorate --oneline --graph
git show <commit-id>
git show <commit-id>:文件名
用来寻找曾经提交、后来被修改或删除的内容。
4. 检查其他引用
git branch -a
git tag
git stash list
git reflog
flag 可能藏在其他分支、标签、stash,或者已经被 reset 掉的提交中。
5. 检查不可达对象
git fsck --full --no-reflogs --unreachable
如果发现悬空对象,可以查看其内容:
git cat-file -t <object-id>
git cat-file -p <object-id>
这一步通常放在最后,因为仓库中的不可达对象可能很多,也可能包含题目故意设置的诱导信息。
不建议一开始执行的命令
分析 Git 取证题时,应尽量保持现场不变。下面这些命令可能改变工作区、暂存区或引用,不适合作为第一步:
git add .
git reset --hard
git clean -fd
git gc
git checkout -- .
其中:
git add .可能用当前工作区状态覆盖暂存区线索;git reset --hard会重置暂存区和工作区;git clean -fd会删除未跟踪文件和目录;git gc可能清理题目刻意保留的不可达对象;git checkout -- .可能改变当前现场。
面对陌生仓库,应该优先执行 status、log、show、diff、fsck 等只读命令。
从这道题学到的内容
这道题并不复杂,但很好地展示了 Git 最容易被初学者忽略的概念:
删除文件不等于删除所有版本
文件可以从工作区消失,但仍然存在于暂存区、历史提交或 Git 对象数据库中。
git add 保存的是内容快照
执行 git add 后,即使继续修改或删除工作区文件,暂存区里的版本也不会自动同步变化。
先读状态,再深入对象
复杂工具不一定应该最先使用。git status --short 中简单的两个字母,有时已经足够指出正确方向。
需要验证线索是否为烟雾弹
CTF 仓库可能故意放置悬空提交、无关 blob 或伪 flag。找到可疑字符串后,还应结合题目提示和仓库状态判断它是否为最终答案。
总结
这道题的完整思路可以概括为:
发现目录只剩 .git
↓
确认题目与 Git 仓库有关
↓
git status --short 发现 MD flag.txt
↓
判断暂存区有内容、工作区文件已删除
↓
git show :flag.txt 读取暂存版本
↓
Base64 解码并按指定格式组合 flag
最关键的命令只有两条:
git status --short
git show :flag.txt
文件在工作区中被删除,并不代表它已经从 Git 中消失。只要理解工作区、暂存区和仓库历史之间的区别,很多看似“被删除”的信息其实仍然有迹可循。


