在文件管理器里看不到一个文件,是否意味着它真的不存在了?

在普通目录中,答案可能是“是”;但在 Git 仓库里,工作区只是其中一层。文件即使已经从工作区删除,它的内容仍可能保存在暂存区、历史提交、其他分支,甚至尚未被清理的 Git 对象中。

这次遇到的 CTF 题目,就是利用了 Git 的这一特点。题目目录表面上只剩一个 .git 文件夹,看起来没有任何可以分析的文件,但真正的 flag 仍保存在 Git 暂存区中。

题目描述

题目给出的提示大意是:

有人使用 Git 藏了一个 flag,你能找到它吗?

打开题目目录后,没有看到普通文件,目录里只剩下:

.git/

这意味着题目重点并不在当前可见文件,而在 Git 保存的仓库信息中。

Git 为什么还能保存被删除的文件

要理解这道题,首先需要区分 Git 中的三个区域:

区域 保存的内容
工作区 当前在文件夹中看到和编辑的文件
暂存区 执行git add 后,准备放进下一次提交的文件快照
本地仓库 已经通过git commit 保存的历史版本

很多人第一次学习 Git 时,会把 git add 理解成“告诉 Git 新增了一个文件”。更准确地说,git add 是把文件当前的内容快照写入暂存区。

因此,完全可能出现这样的状态:

  1. flag.txt 的某个版本已被加入暂存区;
  2. 工作区中的 flag.txt 随后被删除;
  3. 文件管理器中已经看不到它;
  4. 暂存区里仍然保存着删除前的内容。

这正是这道题使用的隐藏方式。

第一步:确认它是不是 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=

由此可以判断:

  1. 冒号后面是一段 Base64;
  2. 解码后的内容需要放进 moectf{};
  3. 这才是题目的真实答案,而不是仓库中其他历史对象里的诱导内容。

第四步:解码 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 中消失。只要理解工作区、暂存区和仓库历史之间的区别,很多看似“被删除”的信息其实仍然有迹可循。