1. 从一个“送分题”说起:git泄露到底在考什么
打CTF的人都有一个共识:Web方向里有一类题目,看起来像是出题人白送分,但每年比赛还是有大把人栽在上面。git泄露就是这类题目的典型代表。我第一次遇到这类题的时候,扫出来一个.git目录,心里想的是“这不就是下载源码吗”,结果折腾了快四十分钟才把flag捞出来。后来复盘才发现,问题不在于思路,而在于对git对象模型和恢复工具链的理解不够细。
这篇文章记录的是我近期做的一道git泄露题目的完整过程,从信息收集、目录探测、源码恢复到最终拿到flag,每一步我都会把“为什么这么做”讲清楚。适合刚接触CTF Web方向的新手,也适合打过几场比赛但每次遇到git泄露都靠工具一把梭、没搞明白底层原理的选手。核心关键词就三个:git泄露、源码恢复、信息收集。读完你至少能掌握两件事:一是遇到.git目录时如何系统性地把源码还原出来,二是当常规工具失效时,怎么手动从git对象里把关键文件抠出来。
先说结论:git泄露的本质不是“下载”,而是“重建”。.git目录里存的不是一份份完整的源码文件,而是一堆压缩过的对象(blob、tree、commit)。你要做的是根据这些对象之间的引用关系,把某个commit对应的完整工作区还原出来。理解了这一点,你就能明白为什么有时候直接访问.git/config能看到东西,有时候却什么都拿不到——因为泄露的程度不同,有的只泄露了部分对象,有的连HEAD引用都没了。
2. 信息收集阶段:怎么发现git泄露的蛛丝马迹
2.1 目录扫描是第一步,但别只依赖字典
拿到题目URL之后,我习惯先做两件事:一是看首页的响应头和页面内容,二是跑一遍目录扫描。这道题的首页是一个很朴素的登录框,没有明显的参数注入点,也没有JS文件里藏提示。这种情况下,目录扫描就是突破口。
我用的是常见的目录扫描工具,字典里包含了.git、.svn、.hg、backup、www.zip这类常见泄露路径。扫描结果很快返回了/.git/HEAD的状态码是200。这里有个细节要注意:不要只看状态码,要看响应内容。有些服务器配置了软404,所有不存在的路径都返回200,但内容是一个统一的错误页。判断方法很简单,访问一个明显不存在的路径比如/thispathshouldnotexist123,对比响应长度和内容。
提示:如果
/.git/HEAD返回200且内容是ref: refs/heads/master这样的引用,基本可以确认存在git泄露。如果返回的是404或者一个HTML错误页,那可能是服务器做了重定向或者目录不存在。
2.2 确认泄露范围:从HEAD到config
确认.git目录存在之后,下一步是判断泄露的完整程度。我通常会按顺序访问这几个文件:
/.git/HEAD:查看当前分支引用/.git/config:查看仓库配置,有时候会暴露远程仓库地址/.git/index:索引文件,包含暂存区的文件列表/.git/logs/HEAD:引用日志,能看到commit历史
这道题的/.git/HEAD返回了ref: refs/heads/master,说明当前在master分支。/.git/config能正常访问,里面没有远程地址,但确认了这是一个标准的git仓库。/.git/logs/HEAD也能访问,里面有几条commit记录。到这里,泄露程度已经比较完整了,理论上可以用工具直接恢复。
但这里有个坑:不是所有git泄露都能用工具一键恢复。有些题目会故意删掉部分对象文件,或者把.git目录做了裁剪,只保留部分内容。所以我的习惯是先用工具跑一遍,如果工具报错或者恢复出来的文件不全,再手动分析。
2.3 工具选型:为什么我最终用了三个工具交叉验证
市面上针对git泄露的恢复工具有不少,我常用的有三个:GitHack、dvcs-ripper和手动git命令。这三个工具各有适用场景:
| 工具名称 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| GitHack | 标准git泄露,对象完整 | 使用简单,自动恢复 | 对不完整仓库支持差 |
| dvcs-ripper | 部分对象缺失 | 容错性好,支持多种VCS | 恢复速度慢 |
| 手动git命令 | 对象残缺严重 | 最灵活,可针对性恢复 | 需要理解git对象模型 |
这道题我先用GitHack跑了一遍,结果报了一个“object not found”的错误,恢复出来的文件只有一部分。这说明仓库对象不完整,可能有部分对象文件被删了。于是我换用dvcs-ripper,它把能下载的对象都拉了下来,但恢复工作区的时候还是缺文件。最后我手动用git命令配合git fsck和git cat-file,从残留的对象里把flag文件抠了出来。
注意:工具不是万能的,尤其是CTF题目里经常故意制造“残缺仓库”的场景。掌握手动恢复的能力,比会用工具更重要。
3. git对象模型速通:理解原理才能手动恢复
3.1 三个核心对象:blob、tree、commit
要手动恢复git泄露,必须理解git的对象模型。git仓库里所有东西都是对象,存在.git/objects目录下,每个对象有一个40位的SHA-1哈希值作为唯一标识。对象分三种:
- blob:存储文件内容,不包含文件名
- tree:存储目录结构,包含文件名和对应的blob或子tree的哈希
- commit:存储一次提交的信息,包含指向tree的哈希、父commit的哈希、作者和提交信息
这三者的关系是:commit指向tree,tree指向blob和子tree,blob存储实际内容。你可以把它想象成一棵树:commit是树根,tree是树枝,blob是树叶。文件名信息只存在于tree对象里,blob本身不知道自己的文件名。
3.2 对象是怎么存储的:zlib压缩与松散对象
git对象默认以“松散对象”的形式存储在.git/objects目录下。具体路径是:取哈希值的前两位作为子目录名,后38位作为文件名。比如一个对象的哈希是a1b2c3...,那它存在.git/objects/a1/b2c3...。
对象内容不是明文存储的,而是经过zlib压缩的。解压之后的内容格式是:<类型> <长度>\0<内容>。比如一个blob对象解压后可能是blob 12\0hello world\n。这个格式很重要,手动恢复的时候需要按这个格式解析。
3.3 为什么工具会失败:对象缺失与引用断裂
工具恢复的原理是:从HEAD指向的commit开始,递归遍历所有tree和blob,把内容还原到工作区。如果中间某个对象文件缺失,遍历就会中断,导致部分文件无法恢复。
这道题的情况是:.git/objects目录下只有部分对象文件,有些blob和tree被删了。但幸运的是,flag文件对应的blob对象还在,只是指向它的tree对象缺失了。这种情况下,工具无法通过正常的引用链找到这个blob,但我们可以通过git fsck命令扫描所有残留对象,找到“悬空”的blob,然后手动解压查看内容。
提示:
git fsck --lost-found命令可以把所有悬空对象输出到.git/lost-found目录,方便后续分析。这是手动恢复残缺仓库的关键技巧。
4. 实操过程:从残缺仓库里捞出flag
4.1 第一步:用dvcs-ripper拉取所有可下载的对象
我先用dvcs-ripper对目标URL跑了一遍,命令大致是这样的:
rip-git.pl -v -u http://target.com/.git/这个工具会尝试下载.git目录下的所有文件,包括objects、refs、logs等。跑完之后,本地会生成一个.git目录,里面包含了能下载到的所有对象。这时候用git status查看,会发现工作区是空的,因为还没有checkout。
4.2 第二步:用git fsck扫描悬空对象
进入恢复出来的.git目录,执行:
git fsck --lost-found输出里会列出所有悬空对象,包括dangling blob、dangling tree、dangling commit。这道题的输出里有几个dangling blob,其中一个的哈希值看起来比较可疑。我用git cat-file -p <哈希>查看内容,发现是一个PHP文件,里面有一行注释写着flag的位置。
这里有个经验:悬空blob的内容往往就是出题人故意留下的线索。因为正常的commit链被破坏了,但这些blob对象还在,它们可能是被删除的commit里的文件,也可能是出题人手动放进去的。
4.3 第三步:手动解压对象文件
如果git fsck也找不到有用的东西,那就需要直接去.git/objects目录里翻。每个对象文件都是zlib压缩的,可以用Python脚本批量解压:
import zlib import os def decompress_git_object(filepath): with open(filepath, 'rb') as f: data = f.read() try: decompressed = zlib.decompress(data) return decompressed except Exception as e: return None objects_dir = '.git/objects' for root, dirs, files in os.walk(objects_dir): for name in files: filepath = os.path.join(root, name) result = decompress_git_object(filepath) if result: # 打印对象类型和内容前200字节 print(f"=== {filepath} ===") print(result[:200]) print()这个脚本会把所有能解压的对象内容打印出来。你可以在输出里搜索flag、ctf、key这类关键词。这道题我就是用这个方法找到了一个包含flag的blob对象。
4.4 第四步:重建tree和commit(可选)
如果题目要求你恢复完整的源码结构,那就需要根据blob和tree的引用关系,手动重建tree对象。这个过程比较繁琐,但原理不复杂:tree对象的格式是<模式> <文件名>\0<20字节的二进制哈希>。你可以用git mktree命令从标准输入创建tree对象。
不过对于大多数CTF题目来说,拿到flag就够了,不需要完整重建仓库。所以这一步我只在需要分析源码逻辑的时候才会做。
5. 常见问题与排查技巧实录
5.1 访问/.git/HEAD返回404怎么办
这种情况可能是服务器配置了规则,禁止访问以点开头的目录。可以尝试以下绕过方法:
- 访问
/.git/HEAD时加上URL编码,比如/%2e%2e/git/HEAD - 尝试
/.git/./HEAD或/.git//HEAD - 如果服务器是Nginx,有时候
/.git/HEAD被拦截但/.git/config可以访问
注意:这些绕过方法仅用于CTF比赛环境,实际渗透测试中需要获得授权。
5.2 工具恢复出来的文件是乱码
这通常是因为对象文件被损坏或者压缩格式不对。可以尝试用git cat-file -t <哈希>查看对象类型,如果是blob,再用git cat-file -p <哈希>查看内容。如果命令报错,说明对象文件本身有问题,可能需要手动用zlib解压。
5.3 git fsck没有输出悬空对象
这说明仓库对象比较完整,没有悬空对象。这时候应该检查HEAD指向的commit是否完整,用git log --all查看所有分支的提交历史。如果commit链完整,直接用git checkout .恢复工作区即可。
5.4 恢复出来的源码里没有flag
flag可能不在源码文件里,而在commit信息、分支名、tag或者git notes里。可以检查以下位置:
git log --all --oneline:查看所有commit信息git branch -a:查看所有分支git tag:查看所有标签.git/refs/目录下的所有文件
这道题的flag其实藏在一条被删除的commit信息里,我是用git fsck --lost-found找到dangling commit之后,用git cat-file -p查看commit内容才发现的。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| /.git/HEAD返回404 | 服务器拦截 | 尝试URL编码绕过 |
| 工具报object not found | 对象缺失 | 用git fsck找悬空对象 |
| 恢复的文件乱码 | 对象损坏 | 手动zlib解压 |
| 源码里没有flag | flag在commit信息里 | 检查git log和dangling commit |
| git fsck无输出 | 仓库完整 | 直接checkout恢复 |
6. 我踩过的坑和几条实用建议
第一个坑是过度依赖工具。我刚开始打CTF的时候,遇到git泄露就是GitHack一把梭,能恢复就恢复,恢复不了就放弃。后来才发现,很多题目的考点恰恰在于“工具恢复不了的时候你怎么办”。手动分析git对象虽然麻烦,但一旦掌握,遇到任何变形的git泄露题目都能应对。
第二个坑是忽略commit信息。很多人恢复出源码之后就直接搜flag,但flag可能藏在commit的message里。我现在的习惯是,恢复完仓库之后先跑一遍git log --all --oneline,看看有没有可疑的提交信息。
第三个坑是不检查所有分支和标签。有些题目会把flag放在一个非master分支上,或者打成一个tag。用git branch -a和git tag检查一下,花不了几秒钟,但可能省下几十分钟。
最后一个建议:自己搭一个git仓库练手。在本地创建一个仓库,提交几次,然后手动删除.git/objects下的部分文件,模拟残缺仓库的场景,练习用git fsck和git cat-file恢复。练上三五次,以后遇到任何git泄露题目都能从容应对。
这个内容后续还可以这样扩展:把git泄露和其他源码泄露类型(比如SVN泄露、HG泄露)做对比分析,整理一份“源码泄露类题目通用解题框架”。另外,如果遇到的是.git目录被打包成zip放在网站根目录的情况,恢复思路又不一样,那个场景更偏向于文件包含和备份文件探测,值得单独写一篇。