GitHack 核心原理与 .git 泄露源码恢复实战指南
简介Git 是现代开发中普遍使用的版本控制工具仓库目录下的 .git 文件夹保存着全部历史对象与索引信息。当 .git 目录被错误发布到 Web 根目录时攻击者无需执行任何代码仅通过 HTTP 请求即可读取对象文件结合 index 索引与 zlib 解压机制还原完整源码。这种 .git 泄露本质上是敏感信息暴露在安全审计中常被用来评估泄露影响、挖掘硬编码凭据或业务逻辑漏洞。GitHack 正是针对该场景的经典恢复工具它读取 .git/index、下载 objects、自动解压帮助 CTF 选手、渗透测试人员和 DevOps 快速验证源码泄露风险。理解对象哈希与索引结构就能在授权测试中高效定位源码恢复结果并完成后续代码审计。1. GitHack-master恢复 .git 泄露源码的最小工具包某个项目在往生产环境同步代码时把整个仓库目录原样丢到了 web 根目录下.git文件夹跟着上了线。对访问者来说这只是多了一个 403 目录但对懂行的人来说这等于把整套源码的「索引卡」递到了手上。GitHack-master 就是干这个的它基于 git 对象与索引的关系把目标站点可访问的/.git/路径里的源码对象逐个拉下来、解压、还原成文件。适合三类人一是打靶场的 CTF 选手拿到提示后要在最短时间内还原站点源码找 flag二是做授权渗透测试的安全工程师需要向客户证明信息泄露的实际影响三是 DevOps想亲眼确认「把 .git 带上线」到底会漏出去多少东西。你不需要先熟读 git 内部文档跟着本文从原理到命令走一遍就能跑通。这份资源包里通常就是一个主脚本加字典文件没有复杂的安装流程拿到手就能干活。2. .git 泄露与源码恢复GitHack 的核心原理与最快跑通路径2.1 先搞懂.git 目录为什么能把源码「吐」出来要理解 GitHack 的原理先得知道 git 是怎么存东西的。一个仓库里.git/objects是所有历史内容的存储区每个文件内容被压缩成一个「blob 对象」对象文件名是内容的 SHA-1 哈希路径按哈希的前两位分目录例如ab/cdef...。真正告诉你「这个仓库有哪些文件」的是.git/index一个二进制索引里面按条目记录了每个被追踪文件的路径、文件大小、修改时间和对应的 blob 哈希。所以整个恢复链条是拿到index就等于拿到一份完整文件清单——路径和哈希都在再按路径去请求objects目录下的对象文件得到字节流后用 zlib 解压文件内容就回来了。git 严格来说不需要「登录」它所有对象都是静态文件服务器只要允许下载这些静态文件恢复就不需要执行任何代码。这也是为什么这类泄露会比源码在页面里被打码更棘手——恢复过程完全不需要目标配合。下面这个小脚本说明了解析 index 的核心逻辑去掉了条目对齐等细节只演示思路import struct data open(index, rb).read() # index 头部4 字节签名(忽略) 4 字节版本号 4 字节条目数 version, count struct.unpack(II, data[4:12]) print(findex v{version}, {count} entries) pos 12 for _ in range(count): # 每条目固定 62 字节最后 20 字节是 blob SHA-1最后 2 字节是 flags ctime_s, ctime_ns, mtime_s, mtime_ns, dev, ino, mode, uid, gid, size, sha, flags struct.unpack( IIIIIIIIII20sH, data[pos:pos 62]) pos 62 end data.index(b\x00, pos) # 路径以 0x00 结尾 path data[pos:end].decode(utf-8, replace) print(path, sha.hex(), size) # 实际还要按 8 字节对齐跳到下一条目这里省略 pos end 1struct.unpack里那串IIIIIIIIII20sH表示大端序、11 个无符号整数加一个 20 字节哈希加一个无符号短整型字段顺序来自 index 格式文档不用死记跑一遍打印出来对照文件清单就明白。实际工具里还包含对 SHA-1 的校验和对象头解析但主线就是「读 index、拿哈希、拼路径、拉对象」。对象文件本身也是 zlib 压缩的。下载到.git/objects/ab/cdef...的字节流后Python 里用zlib.decompress(data)就能得到原始内容解出来的内容最前面是类似blob 1024\0的一段头头之后就是文件本体。GitHack 会自动处理这个解压和去头的过程这也是它比单纯下载静态文件「多出来」的核心价值——没有它你拿到几百个对象文件也不知道谁是谁。2.2 实操从工具包到恢复出第一份源码拿到 GitHack-master 压缩包后先解压确认环境。老版本主脚本要求 Python 2不少新发行版会带 Python 3 修正版。先看一眼脚本头部的 import再决定用python还是python2cd GitHack-master head -n 30 GitHack.py python2 --version || python --version如果脚本里用到urllib2那就是 Python 2 写法如果看到requests或urllib.request说明是可以跑 3.x 的版本。我这边环境只有 Python 3所以会优先找那种带requests的。接下来确定目标。别急着跑大工具先用两三个请求确认泄露真实存在curl -I http://192.0.2.10/.git/HEAD curl -I http://192.0.2.10/.git/index正常响应应该是200 OKContent-Type通常是application/octet-stream或直接无类型如果返回 200 但内容是 HTML 错误页多半是伪静态或 404 指到了统一入口。看到 200 不是 HTML就可以上主脚本了mkdir -p /tmp/githack_out python3 GitHack.py http://192.0.2.10/.git/ -o /tmp/githack_out这个-o参数在部分版本里是输出目录对我拿到的这份包里同样适用如果你手里的包不认-o直接跑python3 GitHack.py http://192.0.2.10/.git/输出会落在当前目录下一个以域名为名的文件夹里。两种都试一次以实际报错为准。跑完后进入输出目录看文件树cd /tmp/githack_out find . -type f | head -n 20 du -sh .如果目录里出现了app/config.py、templates/index.html这类文件说明恢复成功。这里有个经验恢复出来的文件数量只可能小于等于 index 里的条目数因为服务器那侧的 objects 可能不全。先别急着断定失败把文件数记下来后面用git fsck自己补课。关于参数说明URL 尽量写完整路径http://host/.git/结尾的/最好保留部分老实现拼接路径时对斜杠敏感-o不要指向站点目录下的子目录避免把恢复文件又写回你正在扫描的 web 根这个错误我在早期就犯过——输出和输入混在一个目录里二次跑的时候把恢复结果当成了新目标。2.3 看懂恢复结果哪些完整、哪些需要人工补恢复完先看两类东西一类是版本管理自身的文件比如.git/config、.git/HEAD另一类是源码文件。前者只要在输出里出现说明对象下载是通的后者出现但内容不全常见于对象缺失。把恢复出的源码丢进一个空 git 仓库做完整性校验cd /tmp/githack_out git init git config user.email checkexample.com git config user.name check git add . git fsck --lost-found 21 | head -n 20git fsck会列出丢失的对象和悬空对象如果它提示 missing blob那这些 blob 对应的文件就是你后续要手动补的。补文件我一般用「同源公开代码 diff」的办法放到 3.3 节展开。这时最忌讳的是反复重新跑 GitHack——服务器上缺的对象重新拉还是缺白耗时间直接把缺失文件清单导出来更实在。3. 工具包拆解与参数详解Python 2/3 差异、目录选项与公开源码补全3.1 工具包组成一个脚本加几张表就能干活GitHack-master 这类包结构不复杂核心东西我列一下不同打包者会改目录名但角色基本一致常见文件名作用GitHack.py主脚本负责探测目标、解析 index、下载对象、解压写盘wordlist / dict 文件用于目录枚举时猜测文件名部分实现里用于构造候选路径README*参数说明与依赖声明先看它再跑省得猜参数拿到手里先做两件事--help看参数按 README 装依赖。老包常见依赖是requests和click或argparse标准库自带的就不用装。如果 import 环节报No module named xxx直接在虚拟环境里补python3 -m venv venv source venv/bin/activate pip install requests click有的版本只依赖标准库Python 2 时代的包大多是纯标准库那就不需要装任何东西。这个差异本身就是判断包新旧的一个信号纯标准库的包大概率是原版思路依赖requests的通常是后人改造过的版本后者在 Python 3 下更省心。3.2 参数与 URL 构造端口、目录、日志一次讲清主脚本的常见参数在各版本里会有差异但翻来覆去就几类目标 URL、输出目录、日志级别、线程数/超时。以常见用法来说明python3 GitHack.py http://192.0.2.10/.git/ -o ./out --depth 3 --timeout 10--depth控制解析 index 时对路径的截取深度默认值通常是完整路径遇到目标把仓库放在web/子目录、而 index 里记录的是web/app.py这种路径时--depth 1可以帮你剥掉前面被截断的层级。注意这里所谓深度是从 index 里记录的路径开头算起的不是从 URL 的目录级数算的我最早理解反了导致恢复出的目录树里多了一层空壳。--timeout是单对象请求的超时秒数网络差就调大到 20宁可慢一点也别让整个任务挂在某个对象上。部分带并发参数的版本会让你指定--threads或--workers并发数不建议超过 10调太高会被服务端的限流规则盯上触发 429 后恢复速度反而断崖式下降。URL 构造经常在端口和子路径上出事。如果站点开了 8080 端口目标就是http://192.0.2.10:8080/sub/.git/斜杠和端口号之间不要再塞别的目录——有的人会把「站点根」和「git 仓库位置」搞混填成/.git//sub/这种带双斜杠的形式老实现遇到双斜杠会直接拼出错误的下载路径。正确姿势是先去浏览器或 curl 确认 200 的完整 URL再原样贴给工具。日志级别是排查问题的第一抓手。默认情况下工具只打印「下载了哪个文件、失败了几个」信息量不够判断卡在哪。开--verbose或-v后每个对象请求的 URL 和状态码都会出来这时你能清楚看到是 objects 目录整体 403还是某个哈希对象 404。截一段日志[] GET http://192.0.2.10/.git/objects/ab/cdef1234... - 200 [] GET http://192.0.2.10/.git/objects/11/22334455... - 404 [-] missing object: 11223344556677889900aabbccddeeff11223344404集中在某个前缀时基本可以判断服务器上只保留了一部分对象403则说明有规则在拦 objects 目录这时换 URL 大小写、多试几次目录枚举意义不大直接做事后补全更实际。3.3 进阶玩法用公开代码补全服务器上缺失的对象GitHack-master 的价值不只是「在线恢复」它更大的用处是帮你定位「到底改了哪些文件」。具体做法是如果你确认目标跑的是一个开源项目把对应公开仓库克隆下来用 GitHack 恢复出的文件路径做一次 diff缺失的文件从公开仓库对应路径直接拷贝从而得到一个相对完整的工作区。一个我常用的比对脚本长这样import os, shutil base_recovered out # GitHack 恢复出的目录 base_public public_repo # 公开仓库克隆目录 base_merged merged # 合并输出目录 os.makedirs(base_merged, exist_okTrue) for root, _, files in os.walk(base_public): for name in files: rel os.path.relpath(os.path.join(root, name), base_public) recovered os.path.join(base_recovered, rel) target os.path.join(base_merged, rel) if os.path.exists(recovered): os.makedirs(os.path.dirname(target), exist_okTrue) shutil.copy2(recovered, target) # 优先生成恢复出来的版本 else: os.makedirs(os.path.dirname(target), exist_okTrue) shutil.copy2(os.path.join(root, name), target) missing 0 for root, _, files in os.walk(base_public): for name in files: rel os.path.relpath(os.path.join(root, name), base_public) if not os.path.exists(os.path.join(base_recovered, rel)): missing 1 print(fmissing in recovered: {missing})这段脚本的逻辑是「恢复版优先公开版兜底」。为什么要恢复版优先因为目标站点跑的很可能是改过代码的版本公开仓库只是骨架直接拿公开版覆盖会把对方本地改动的痕迹弄丢。反过来缺失的文件大多是没被改动过的公共依赖直接补回来不会影响判断。跑完再git diff一下恢复版和公开版差异列表就是你最想要的「泄漏分析报告」。这套方法适合「确认目标源于某个公开项目」的场景目标闭源时就不适用了——那种情况只能把存在的对象尽量挖出来并如实告诉客户完整的缺失清单等于「文件被删除/从未提交」的边界。我一般会在报告里明确区分「已恢复」「对象缺失可补充」「对象缺失且来源不可考」三档避免客户误以为恢复结果是百分百完整。4. 避坑指南GitHack 使用中的常见问题与排查记录这一章记录的全是我实际跑 GitHack 时踩过的坑按「现象 → 原因 → 解决」的顺序写每条都能直接对应到具体操作。4.1 老包在 Python 3 下直接翻车SyntaxError 和 UnicodeDecodeError现象在较新系统上执行python GitHack.py ...秒报SyntaxError: invalid syntax或者跑到一半报UnicodeDecodeError: gbk codec cant decode byte ...。原因GitHack 早年流行版本是为 Python 2 写的print语法、字符串编码都依赖 2.x 的语义Windows 控制台默认编码是 GBK遇到 UTF-8 路径直接炸。这不是你环境装坏了是包本身和解释器版本不匹配。解决先看头部 import有urllib2就换python2运行没有 Python 2 就选带urllib.request的 3.x 修正版。临时救急可以在启动时加环境变量PYTHONIOENCODINGutf-8但语法层面的报错只能换解释器或换包别在代码里打补丁。4.2 页面 403但你的恢复流程还有戏现象浏览器访问/.git/返回 403 Forbidden以为没戏直接放弃整个目标。原因服务器只是禁止了目录列举但静态文件直出策略把.git/index、.git/HEAD暴露了出来也可能是 403 页面来自统一入口伪静态实际静态文件由另一层服务处理。两种情况的共同点是目录看不了但文件能下。解决用curl -I单独探测.git/HEAD和.git/index两个里有一个返回 200 就继续跑只有/.git/目录列表 403 完全不影响 GitHack 工作它本来就不靠列目录。我后来形成习惯任何目标先默认测这两个文件而不是测目录本身。4.3 恢复出一堆空目录objects 全是 404现象文件清单打印出来了但几乎所有对象请求都是 404输出目录里只有嵌套空目录没有实际文件。原因服务器只暴露了 index没有暴露 objects。这是很常见的「半泄露」——某次备份把.git完整拷上了线但安全设备或配置只准读 index 后缀的文件或者同步时只带了部分对象。解决确认/.git/objects/的响应码如果整个 objects 目录统一 403/404恢复工具本身没救直接跳到上一章的公开代码补全流程或者向目标方确认该仓库的公开来源。不要在工具参数上反复折腾参数改再多也换不来服务器上不存在的文件。4.4 子路径部署目标不是根目录的 .git现象目标站点的代码在http://host/sub/工具默认请求/.git/HEAD一路 404但目标明明有 git 泄露。原因git 仓库的.git在子目录里工具没有自动定位仓库根的能力。多数实现默认从 URL 原样拼接不会帮你往上级目录试探。解决先用 curl 手测http://host/sub/.git/HEAD是否 200是的话把完整 URL 给工具。部分实现支持--root或--base参数意义就是「告诉工具仓库根在哪」看--help确认包支持哪个。手动探测这一步别省直接决定后续所有请求的路径是否成立。4.5 大仓库下到一半断掉重跑又从零开始现象跑了几千个对象网络抖动一下任务中断重跑时前面的下载对象又重复拉了一遍浪费时间也加重服务器负担。原因脚本默认不做本地缓存每个对象下载后只写文件再次启动不会检查磁盘上是否已有同名对象。断点续传在这个工具里基本是奢望。解决中断后不要急着重跑先把已下载的对象按objects/ab/cdef...结构归档再跑时用小脚本统计「已在本地存在的 object 数」确认大部分对象已在磁盘上剩下的单独用一个只拉缺失对象的循环处理。这样即使服务器那侧限速也能分多次把大仓库搬完。统计脚本很简单就是遍历本地已下载文件再对比目标 index 里的哈希清单两边一减就知道还缺哪些。5. 把 GitHack 用得更顺手批量检测与结果校验工具单次跑一个目标很容易但实际工作中往往面对几十个候选域名。与其一个个手动执行不如写一个批量检测脚本把「探测 → 恢复 → 校验」三步串起来。#!/bin/bash LISTtargets.txt # 每行一个目标 URL例如 http://192.0.2.10/.git/ OUT_ROOTgithack_results mkdir -p $OUT_ROOT while read -r url; do [ -z $url ] continue name$(echo $url | sed s|http[s]*://||; s|[/:]|_|g) out_dir$OUT_ROOT/$name [ -d $out_dir ] continue # 已跑过的目标跳过天然支持断点 # 第 1 步先用 HEAD 请求确认 .git/HEAD 可访问 code$(curl -s -o /dev/null -w %{http_code} -I $url/HEAD --connect-timeout 5) if [ $code ! 200 ]; then echo $url - skip (HTTP $code) continue fi # 第 2 步跑 GitHack输出到独立目录 mkdir -p $out_dir python3 GitHack.py $url -o $out_dir --timeout 10 $out_dir/run.log 21 # 第 3 步校验恢复文件数少于阈值标记为可疑 count$(find $out_dir -type f | wc -l) echo $url - files: $count done $LIST这套脚本里有两个习惯值得保留第一用curl前置探测过滤掉无效目标避免给每个 404 站点都跑一遍完整流程第二输出目录以域名加端口命名避免多个目标恢复结果互相覆盖。[ -d $out_dir ] continue这行是关键脚本中断后重跑时已经恢复过的目标会直接跳过配合上一章提到的「对象归档」思路基本上做到了轻量级断点续传。结果校验我固定走三步先find统计文件数看有没有恢复出实体文件再git fsck --lost-found看对象完整度missing blob 数量超过文件总数 30% 时我会在报告里降级标注最后随机挑 3 个源码文件打开看头部确认是真实代码而不是 404 页面。校验这一步经常能发现工具误报——有的服务器对不存在路径统一返回 HTML 首页加 200 状态码工具会以为下载成功实际写进磁盘的是错误页文件数不少但一个能用的都没有。遇到这种情况我会在脚本里加一层「内容嗅探」对恢复出的文件做file命令检查如果大量文件显示HTML document而目标本身不是纯静态站基本可以判定是统一 200 响应在捣鬼这时候要把目标从清单里剔掉重新评估。最近一次做演练时我就是靠这套脚本在半小时内扫完了六十多个候选地址筛出三个真实存在.git泄露的目标其中一个的 objects 完整度极高恢复出来的源码直接通过了客户验收。从那以后我每次拿到这类工具包都会先写一个targets.txt把目标管理起来再谈参数调优——工具本身只负责单点突破批量场景靠的是流程。希望帮到你。本文还有配套的精品资源点击获取