从 Git 原理到 Git 泄露利用:一次学习记录

Aleph_null Lv1

最近做 CTF Web 题时遇到了 Git 泄露。一开始我只是知道 Git 可以保存代码版本,平时会用 git addgit commitgit log,但真正遇到 .git 泄露题时才发现:如果不理解 Git 底层是怎么保存文件的,就很容易只会跑工具,而不知道为什么能恢复源码、为什么删除过的文件还能找回来。

这篇文章主要记录两个重点:一个是 Git 的底层原理,另一个是 Git 泄露在 CTF 中为什么能被利用,以及应该怎么分析。

Git 保存的不是普通文件夹,而是一套对象数据库

平时看一个 Git 仓库时,我们看到的是普通项目目录,比如:

1
2
3
4
5
project/
├── index.php
├── config.php
├── README.md
└── .git/

真正重要的是 .git/ 目录。它不是一个无关紧要的隐藏文件夹,而是整个 Git 仓库的核心数据库。Git 的历史提交、分支、标签、暂存区信息、对象数据,基本都放在这里。

Git 底层最核心的结构可以概括成一句话:

1
HEAD → branch → commit → tree → blob

这里面最关键的是 committreeblob 三种对象。

blob 保存文件内容。它只关心内容本身,不关心文件名。比如 a.txtb.txt 内容完全一样,它们可以指向同一个 blob。

tree 保存目录结构。因为 blob 不知道自己叫什么名字,所以文件名、目录名、哪个路径对应哪个 blob,这些信息由 tree 负责保存。

commit 保存一次提交。它指向一个根 tree,同时记录上一个 commit、作者、提交时间、提交信息等内容。

所以一次提交大概可以理解成:

1
2
3
4
5
6
commit

tree
├── index.php → blob
├── config.php → blob
└── src/ → tree

这也是 Git 能恢复历史版本的根本原因。每一次 commit 都能找到当时的目录树,目录树又能找到当时每个文件对应的内容。只要这些对象还在 .git/objects/ 里,Git 就有机会把某个历史状态重新还原出来。

git addgit commit 背后发生了什么

以前我一直觉得 git add 只是“把文件加入 Git”,但这个说法其实不准确。

当执行:

1
git add a.txt

Git 会读取 a.txt 当前的内容,把它做成一个 blob 对象,压缩后存入 .git/objects/。同时,Git 会把“a.txt 这个路径对应哪个 blob”记录到 .git/index 里。也就是说,git add 之后,文件内容其实已经进入 Git 的对象数据库了,只是还没有变成正式提交。

这点在 CTF 中很重要。因为有些文件可能只被 git add 过,没有被 git commit,但它的内容已经作为 blob 留在 objects 里了。这种 blob 如果后来没有被 commit 引用,就可能变成 dangling blob,仍然有机会通过 git fsck 找出来。

而执行:

1
git commit -m "add a"

Git 会根据暂存区 .git/index 生成 tree,再根据 tree 生成 commit,最后让当前分支指向这个新的 commit。

所以整个保存过程可以理解为:

1
2
3
4
5
6
7
工作区文件
↓ git add
blob 对象 + index 记录
↓ git commit
tree 对象 + commit 对象

当前分支指向新 commit

这也是为什么 Git 不只是保存“当前文件”,而是保存了一整套可以追溯的历史结构。

.git 目录为什么这么敏感

.git 目录里面有几个非常关键的文件。

.git/HEAD 表示当前仓库位置。常见内容是:

1
ref: refs/heads/main

意思是当前 HEAD 指向 main 分支。

.git/refs/heads/main 里面通常保存当前分支最新提交的 commit hash。

.git/objects/ 里面保存真正的 Git 对象,包括 commit、tree、blob。

.git/index 是暂存区文件,里面记录文件路径和 blob 的对应关系。

.git/logs/HEAD 保存 HEAD 移动历史,也就是 reflog。它可能记录一些普通 git log 看不到的旧 commit。

所以如果一个网站暴露了 .git 目录,泄露的就不只是当前源码,而是整个 Git 仓库的历史数据库。当前版本里删掉的文件,历史提交里可能还有;当前分支看不到的提交,reflog 里可能还有;甚至只 add 过没 commit 的文件,也可能以 dangling blob 的形式残留在 objects 中。

这就是 Git 泄露危险的地方。

Git 泄露的判断方式

在 CTF 中,判断一个网站是否存在 Git 泄露,最常见的方式是访问:

1
/.git/HEAD

如果返回:

1
ref: refs/heads/master

或者:

1
ref: refs/heads/main

基本就可以确定 .git 目录存在泄露。

这里要注意,不能只访问:

1
/.git/

因为服务器可能禁止目录浏览,访问 .git/ 返回 403,但具体文件比如 .git/HEAD.git/index 仍然能访问。

一旦 .git/HEAD 能读,后续就可以继续尝试读取:

1
2
3
4
5
6
7
/.git/config
/.git/index
/.git/refs/heads/main
/.git/refs/heads/master
/.git/packed-refs
/.git/logs/HEAD
/.git/objects/info/packs

这些文件分别可以帮助我们确认分支、找到 commit hash、恢复文件列表、查找历史提交和下载 Git 对象。

Git 泄露为什么能恢复源码

假设访问:

1
/.git/HEAD

得到:

1
ref: refs/heads/main

再访问:

1
/.git/refs/heads/main

得到一个 commit hash:

1
84ed678cfb7099283a742008c0b5bed963c09e88

这个 hash 对应的对象路径是:

1
.git/objects/84/ed678cfb7099283a742008c0b5bed963c09e88

下载这个对象后,可以用:

1
git cat-file -p 84ed678cfb7099283a742008c0b5bed963c09e88

查看内容。如果它是 commit 对象,里面会有类似:

1
2
3
4
5
tree 5f1a2b3c...
parent 12ab34cd...
author admin <admin@example.com> ...

remove flag

这里的 tree 指向当前提交对应的目录结构。继续下载 tree 对象,就能看到文件名和 blob hash:

1
2
3
100644 blob aaaaaaaa... index.php
100644 blob bbbbbbbb... config.php
040000 tree cccccccc... src

再下载 blob 对象,就能拿到具体文件内容。

所以 Git 泄露恢复源码的本质不是“扫描网站目录”,而是沿着 Git 的对象关系链一步步还原:

1
HEAD → refs → commit → tree → blob → 文件内容

自动化工具,比如 GitHack、GitTools,本质上也是在做这件事。工具可以帮我们快速下载 .git 里的关键文件和对象,但理解这条链之后,就算工具失败,也能手动分析。

CTF 中真正要看的不只是当前源码

很多 Git 泄露题不会把 flag 直接放在当前源码里。更常见的情况是:flag 曾经出现过,后来被删除了。

比如历史可能是:

1
2
3
init
add flag
remove flag

当前版本看不到 flag.txt,但 add flag 那个提交里可能还存在。甚至 remove flag 这个提交的 diff 里,也可能直接显示被删除的内容。

这时应该看:

1
git log --oneline --all --decorate --graph

如果看到可疑提交,比如:

1
2
a3c9f12 remove flag
84ed678 add flag

可以直接查看:

1
git show a3c9f12

如果这是删除 flag 的提交,diff 里可能出现:

1
-AAA{git_history_is_dangerous}

也可以直接查看旧版本文件:

1
git show 84ed678:flag.txt

这比一个个切换版本更方便。

reflog 和 dangling object 往往是关键

有些题会更进一步。出题人可能先提交 flag,然后用:

1
git reset --hard HEAD~1

把这次提交从当前分支历史里移掉。这样普通的 git log 可能看不到那个 commit。

但如果 .git/logs/HEAD 还在,就可以通过 reflog 找到 HEAD 曾经指向过的 commit:

1
git reflog

或者直接查看:

1
cat .git/logs/HEAD

如果里面出现:

1
2
commit: add flag
reset: moving to HEAD~1

就说明曾经有一个提交可能包含 flag。找到对应 commit hash 后,用 git show 查看即可。

另外一种情况是 dangling object。比如某个文件被 git add 过,但没有 commit;或者某个 commit 被 reset 掉了,但对象还没有被 Git 清理。这时可以用:

1
git fsck --full

如果看到:

1
2
dangling blob 9a1b2c3d...
dangling commit 84ed678...

就可以继续用:

1
2
git cat-file -p 9a1b2c3d...
git show 84ed678...

查看内容。CTF 中有些 flag 就直接藏在 dangling blob 里。

我的 Git 泄露分析思路

现在遇到 Git 泄露题,我不会只停留在“跑工具恢复源码”。更合理的思路应该是先确认 .git/HEAD 是否泄露,然后尽量恢复仓库;恢复后先搜索当前源码,再查历史提交。如果当前历史没有结果,就继续查 reflog、dangling object、其他分支、tag、stash、.gitignore.git/config

当前源码可以用:

1
grep -RniE "flag|AAA\{|secret|password|token|key" .

所有历史可以用:

1
git grep -n -i -E "flag|AAA\{|secret|password|token|key" $(git rev-list --all)

可疑提交用:

1
git show <commit>

历史文件用:

1
git show <commit>:<path>

悬空对象用:

1
2
git fsck --full
git cat-file -p <hash>

这些命令背后的逻辑其实都一样:围绕 Git 对象数据库找内容,而不是只看当前工作区。

总结

这次学习之后,我对 Git 的理解变得更清楚了。Git 的核心不是简单保存文件副本,而是通过 commit、tree、blob 这些对象,把项目的每一次状态组织成一套可以追溯的历史数据库。

也正因为这样,.git 泄露才会这么严重。它暴露的不只是当前源码,而是可能包含历史源码、删除文件、reset 前提交、暂存过的 blob、分支、tag、stash 和配置信息。

所以做 Git 泄露题时,重点不是死记工具,而是理解:

1
HEAD → refs → commit → tree → blob

这条链。

理解了这条链之后,就能明白为什么访问 .git/HEAD 可以作为入口,为什么 commit hash 能继续找到 tree,为什么 tree 能恢复文件名,为什么 blob 能还原文件内容,也能理解为什么删除过的 flag 仍然可能在 Git 历史中被找回来。

  • 标题: 从 Git 原理到 Git 泄露利用:一次学习记录
  • 作者: Aleph_null
  • 创建于 : 2026-07-09 00:00:00
  • 更新于 : 2026-07-09 14:42:54
  • 链接: https://aleph-null.cc/2026/07/09/git-leak-learning-record/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。