源码泄露详解——.git、.svn、.hg 的原理、利用与工具
目录里有源码,不代表只有当前版本。Web 目录下出现 .git、.svn 或 .hg 这样的版本库元数据目录,说明部署时没剥离干净,探测到其中一个,往往能把整个开发历史、数据库密码和被注释掉的测试接口一起带回来。
先看最直接的一步探测:
1 | GET /.git/HEAD |
返回 200 且内容是 ref: refs/heads/master,说明站点根目录下挂着一个 Git 仓库,.git 没被整体拦掉。不过一个文件可读推不出整个目录可读,接下来要确认 objects/ 和 index 能不能拿到——那才是恢复源码的依据。
成立条件与原理
三类泄露的成因相同,三条同时满足即可利用:
- 版本库元数据目录被部署进了 Web 根目录——
rsync/scp整目录上传、IDE 直接发布、Git 挂目录部署都可能出现 - Web 服务器没有对以点开头的目录做访问控制
- 版本库对象文件可读——恢复源码靠的就是逐个读取对象,不是读目录列表
| 版本系统 | 泄露目录 | 关键探测点 | 可恢复内容 |
|---|---|---|---|
| Git | /.git/ |
HEAD、config、logs/HEAD、index、objects/ |
完整提交历史与工作区 |
| SVN | /.svn/ |
entries(1.6 及以前)、wc.db(1.7 起) |
工作区文件与基线版本 |
| Mercurial | /.hg/ |
requires、store/00manifest.i |
完整变更历史 |
SVN 1.7 重构了工作副本格式:entries 变成只有一行版本号的空壳,目录树和文件校验信息全部收进 wc.db 这个 SQLite 文件。老资料里”读 entries 遍历目录”的方法只适用于 1.6 及以前的格式,现在实际遇到的大多是 wc.db。
探测与识别
手工确认用 curl 逐个打探测点,比目录爆破快:
1 | curl -s http://example.com/.git/HEAD |
第二条下载下来是 SQLite 3 database 即确认 SVN 泄露;requires 文件是 Mercurial 仓库的固定配置文件,返回 200 即确认。批量扫站时 dirsearch 的默认字典自带这些条目,适合前期资产梳理阶段顺手过一遍。
/.git/ 返回 403 不等于不可利用——很多配置只禁了目录索引,HEAD、objects/ 下的单个文件照样可读。判定以逐文件探测为准,不要在目录列表这一步放弃。
.git 的利用
核心思路一句话:让本地 git 把远程 .git 目录当成一个 HTTP 协议的远程仓库。前提是目录里有 info/refs——git 靠它拿到引用列表,缺这个文件时 fetch 会报 repository not found,看着像地址写错,其实对象都在、引用清单没生成。
手工恢复
1 | git init src && cd src |
命令逐条解释
git remote add origin http://example.com/.git/:把泄露目录当作远程仓库地址。dumb HTTP 靠逐个下载 objects/ 下的文件工作,前提是先有 info/refs 给出引用列表(服务器执行过 git update-server-info 才会生成);缺它时这一步直接失败,得改用 GitHack 逐文件抓git fetch origin:拉取全部对象与引用,不改动工作区git reset --hard origin/master:把当前分支指向拉下来的提交并还原全部文件。分支名以/.git/HEAD里的ref:为准,可能是master也可能是main或其他
工具:GitHack
手工流程封装一下就是 GitHack(BugScanTeam 出品),自动解析 HEAD 与引用、逐个拉取对象、按 index 还原文件树:
1 | python GitHack.py http://example.com/.git/ |
同类还有 gakki429 的 Git_Extract,补的是 GitHack 容易漏的几类文件——logs/HEAD、refs/stash、info/packs;pack 文件里 delta 对象的重建还挂在 README 的待做清单上,指望它补 pack 内部对象会落空。GitHack 是 Python 2 脚本且要求本机有 git 命令,两个项目都得整仓库 clone 后运行,没有单文件即用这回事。
提交历史与 stash
仓库恢复后,历史里的东西往往比工作区当前版本值钱:
1 | git log --oneline --all # 全部提交 |
logs/HEAD 记录了本地引用变动,被回滚掉的提交和 stash 操作在这里留有痕迹——工作区里看不到的数据库密码、内网接口,经常藏在这些”删掉了”的提交里。CTFhub 把这条线拆成了 log、stash、index 等变体关卡,每种变体对应不同文件的恢复手法,适合把流程练熟(CTFhub 技能树 Web 的「信息泄露」章节)。
.svn 与 .hg 的利用
.svn:从 wc.db 到 pristine 基线文件
SVN 工作副本里,文件本体按 SHA1 校验值存在 pristine 目录:/.svn/pristine/<校验值前两位>/<校验值>.svn-base。恢复思路是先拿目录索引,再按索引下载文件:
1 | wget http://example.com/.svn/wc.db |
目录树大时手工会很累,dvcs-ripper 的 rip-svn.pl 自动完成”读 wc.db 的 pristine 表 → 批量下载基线文件 → 调本地 svn 还原文件名”全过程:
1 | ./rip-svn.pl -v -u http://example.com/.svn/ |
-v 显示进度。脚本依赖 Perl 及 LWP、IO::Socket::SSL、DBD::SQLite 几个模块,外加本机 svn 客户端(收尾会调 svn revert 把基线还原成文件名);Kali 自带 Perl,但工具本体和模块要自己装。1.6 及以前的旧格式(明文 entries)它同样支持,遇到老系统直接跑即可。
.hg:整目录下载后本地重建
Mercurial 的存储是 store/ 目录下的 revlog 文件:00changelog 承担提交日志,00manifest 承担文件清单,各文件的内容按文件名哈希存放在 store/data/ 下。它没有 SVN 那样的单一索引文件,利用方式就是把 .hg 目录完整拉下来,本地重建仓库:
1 | ./rip-hg.pl -v -u http://example.com/.hg/ |
rip-hg.pl 同属 dvcs-ripper,按 changelog 和 manifest 的线索递归补齐 store/data/ 下的文件;verify 通过说明对象完整,后面就是正常的本地仓库操作。
边界与危害评估
能拿到什么,取决于对象文件的完整程度:
- Git/Hg 的历史完整度看部署方式:整目录上传或 clone 部署一般带全量对象,历史就是全的——全部源码版本、被删除的调试文件、硬编码的密钥与数据库口令、注释里的内网地址和后台路径
- SVN 只能恢复 checkout 时点的基线版本,仓库本体(含完整历史)在另一台服务器上,这里拿不到
- 目录被运维清过一部分时(只剩 pack 没有 idx、pristine 缺文件),恢复结果是不完整的,按能拿到的部分评估即可
这类操作通常会在访问日志里留下大量 /.git/、/.svn/ 的请求记录,真实目标上只做授权范围内的验证。CTF 之外的实战价值主要在众测和 SRC 的信息收集阶段——拿到源码后后续攻击面(未授权接口、默认口令、依赖漏洞)会宽很多。
防御
两层各做一件事,缺一个都防不住:
- 部署剥离:构建产物单独打包发布,VCS 元数据不进 Web 目录。rsync 加
--exclude排除、CI 流水线里用git archive或构建产物部署都可以;.gitignore 只管提交不管部署,指望它挡不住 - 服务器兜底:Web 层直接拒绝点开头的路径
1 | location ~ /\.(git|svn|hg) { |
自己的资产定期扫一遍 /.git/HEAD 这类探测点,部署流程有变(换了发布工具、加了镜像同步)之后重点复查。
速查表
| 线索 | 确认方式 | 恢复手段 |
|---|---|---|
/.git/HEAD 返回 ref: |
curl 逐文件探测 |
GitHack,或 git fetch + reset --hard |
/.git/ 目录 403 |
403 只挡索引,逐文件再测 | GitHack 仍可用,对象可读即可 |
/.git/logs/HEAD 有 stash/reset 记录 |
git log --all、stash list |
git checkout <hash> 查历史提交 |
/.svn/wc.db 为 SQLite |
file 命令确认 |
rip-svn,或 sqlite3 查 NODES + 下载 pristine |
/.svn/entries 有明文内容 |
1.6 旧格式 | rip-svn 直接支持 |
/.hg/requires 返回 200 |
curl |
rip-hg 整目录下载,hg verify + hg update |





