简介GitHack-master是一款面向安全研究人员与开发者的Git源码泄露检测与利用工具专用于发现并分析Web服务器意外暴露的.git目录解决因版本控制文件误公开导致的历史代码、敏感信息及漏洞线索泄露问题。资源包共14个文件含10个Python脚本如GitHack.py主程序、git.py核心解析模块、log.py日志组件等、1份LICENSE授权文件、1篇README.md说明文档、1个user-agents.txt配置文件及1个.gitignore整体仅41KB轻量便携无需额外依赖仅需系统预装Git命令行即可运行。已有3171人学习下载适用于渗透测试实操、代码安全审计、CTF Git类题目复现及DevSecOps自查场景。读者可直接获取完整可执行工具链、清晰的模块化代码结构、.git目录逆向解析逻辑及敏感信息提取能力快速掌握从泄露识别到源码恢复的全流程技术要点。1. GitHack-master一个专治「Git 仓库被意外暴露」的本地化取证工具你有没有遇到过这样的场景上线前匆忙打包忘了删掉.git目录测试环境配置疏忽Nginx 未禁用.git路径访问甚至 CI/CD 流水线里某条rsync命令多同步了一个--include.git/**结果——线上站点根目录下/.git/HEAD可直访/.git/config明文泄露/.git/logs/refs/heads/master里还躺着三天前 commit 的邮箱和密钥片段。这不是理论风险是真实发生过数十起的生产事故。GitHack-master 就是为这类「Git 泄露已成事实、但尚未被攻击者批量利用」的紧急响应场景而生它不依赖网络扫描器不调用云 API纯本地运行5 秒内从一个可访问的.git目录完整还原出原始源码树含所有历史版本、分支、未 push 的暂存变更支持断点续爬、路径过滤、文件去重且全程不发任何 HTTP 请求——所有数据都来自你本地已下载的.git文件包。适合运维同学做应急溯源、安全工程师做红队复盘、开发自查部署包完整性也适合 CTF 比赛中快速提取靶机源码。它不是渗透工具而是「Git 仓库结构解析器 历史快照重建器」的组合体。2. 架构设计与核心模块拆解从.git目录到可读源码的三步转化GitHack-master 的本质是把 Git 的底层对象模型blob/tree/commit/tag翻译成开发者熟悉的文件系统结构。它不启动 Git 进程不调用git checkout或git restore而是直接解析.git/objects/下的 zlib 压缩对象、.git/refs/中的引用指针、.git/index中的暂存区索引——这种「绕过 Git CLI直读对象数据库」的设计让它在目标环境无 Git 环境、或 Git 版本不兼容时仍能工作也避免了命令注入风险。整个流程分三阶段定位起点 → 解析图谱 → 重建文件。下面逐层拆解其核心模块实现逻辑。2.1 入口控制与参数解析githack.py的初始化逻辑项目主入口是githack.py其参数设计直击实战痛点。关键参数包括-u/--url接受两种格式——http://target.com/.git/自动补全路径并下载或./local_git_dir/本地模式跳过下载直接解析-o/--output指定输出目录默认为./recovered/-d/--depth控制递归深度默认3足够覆盖绝大多数分支和 tag过高易触发内存溢出-f/--filter支持 glob 模式过滤如-f *.py,config/*.yaml避免恢复大量日志或编译产物--no-commit跳过 commit 对象解析仅恢复最新 HEAD 对应的 tree提速 40%# githack.py 核心参数解析段简化版 import argparse import os def parse_args(): parser argparse.ArgumentParser(descriptionRecover source code from exposed .git directory) parser.add_argument(-u, --url, requiredTrue, helpURL or local path to .git directory) parser.add_argument(-o, --output, default./recovered, helpOutput directory for recovered files) parser.add_argument(-d, --depth, typeint, default3, helpMax depth for commit history traversal) parser.add_argument(-f, --filter, default, helpComma-separated glob patterns to include (e.g., *.py,config/*)) parser.add_argument(--no-commit, actionstore_true, helpSkip commit history, recover only current HEAD tree) return parser.parse_args() if __name__ __main__: args parse_args() # 后续根据 args.url 判断是远程下载还是本地解析提示--filter参数的实现不是简单fnmatch而是构建了一个轻量级白名单树。例如-f src/*.js,README.md会被转为两个路径前缀src/和README.md后续在遍历 tree 对象时只对匹配前缀的 blob 节点执行解压写入其余跳过。这比下载后rm -rf更高效尤其对千级文件的仓库。2.2 对象解析引擎objects.py如何解压并反序列化 Git 对象Git 对象存储在.git/objects/xx/xxxxxx...中每个文件是 zlib 压缩后的二进制流格式为type size\x00content。objects.py模块负责这个「黑匣子」的逆向工程。它不依赖git cat-file而是用纯 Python 实现解压与解析# objects.py 关键函数parse_object_from_file import zlib import struct def parse_object_from_file(obj_path): 从 .git/objects/xx/xxxxxx 文件中解析 Git 对象 返回: (object_type, content) 例如 (blob, bprint(hello)\n) try: with open(obj_path, rb) as f: raw_data f.read() # Git 对象是 zlib 压缩的先解压 decompressed zlib.decompress(raw_data) # 解析头部type space size null byte null_pos decompressed.find(b\x00) if null_pos -1: raise ValueError(Invalid git object: no null byte found) header decompressed[:null_pos] body decompressed[null_pos1:] parts header.split(b , 1) if len(parts) ! 2: raise ValueError(fInvalid header format: {header}) obj_type parts[0].decode(ascii) obj_size int(parts[1]) # 验证解压后内容长度是否匹配声明大小防篡改/损坏 if len(body) ! obj_size: raise ValueError(fSize mismatch: declared {obj_size}, got {len(body)}) return obj_type, body except Exception as e: # 记录错误但不中断Git 仓库可能含损坏对象如部分上传 print(f[WARN] Failed to parse {obj_path}: {e}) return None, None这段代码的关键在于它把 Git 对象当作「带校验头的压缩包」来处理而非调用外部命令。zlib.decompress()是 Python 标准库零依赖header.split(b , 1)精确切分 type 和 size避免正则开销len(body) ! obj_size校验是血泪经验——曾有客户因 CDN 缓存截断导致部分对象 size 声明为 1024 但实际只有 512 字节没这层校验会静默写入损坏文件。2.3 历史图谱构建commit.py的 DAG 遍历与环路规避Git 的 commit 是有向无环图DAG但.git/refs/下的引用如refs/heads/main,refs/remotes/origin/feature只是指向某个 commit hash 的文本文件。commit.py的任务是从这些引用出发递归解析 parent commit构建完整的可达性图谱。难点在于如何避免无限循环Git 本身靠 SHA-1 哈希保证唯一性但解析器需主动记录已访问节点。# commit.py 核心遍历逻辑 from collections import deque def build_commit_graph(refs_dir, objects_dir, max_depth3): 从 refs 目录构建 commit 图谱返回 {commit_hash: {parents: [...], tree: xxx, message: ...}} 使用 BFS 控制深度用 visited_set 防环 graph {} visited set() queue deque() # 1. 从所有 refs 文件读取初始 commit hash for ref_path in get_all_ref_files(refs_dir): # 遍历 .git/refs/ 下所有文件 try: with open(ref_path, r) as f: commit_hash f.read().strip() if len(commit_hash) 40 and commit_hash.isalnum(): # 粗略校验 SHA-1 queue.append((commit_hash, 0)) # (hash, depth) except: continue # 2. BFS 遍历限制深度 while queue: curr_hash, depth queue.popleft() if depth max_depth or curr_hash in visited: continue visited.add(curr_hash) obj_path get_object_path(objects_dir, curr_hash) # 转为 .git/objects/ab/cdef... obj_type, content parse_object_from_file(obj_path) if obj_type ! commit: continue # 解析 commit 对象内容格式为 tree xxx\nparent yyy\nauthor ...\ncommitter ...\n\nmessage lines content.split(b\n) tree_hash None parents [] message for line in lines: if line.startswith(btree ): tree_hash line[5:].decode(ascii).strip() elif line.startswith(bparent ): parents.append(line[7:].decode(ascii).strip()) elif line b: # 空行后是 message message b\n.join(lines[lines.index(line)1:]).decode(utf-8, errorsignore) break graph[curr_hash] { tree: tree_hash, parents: parents, message: message[:200] # 截断长 message避免日志爆炸 } # 将 parent 加入队列深度1 for p in parents: if p not in visited and len(p) 40: queue.append((p, depth 1)) return graph这里用deque实现 BFS 而非 DFS是因为BFS 天然按提交时间倒序新 commit 在前且深度控制更直观visited集合用set()而非list确保 O(1) 查重errorsignore处理 message 中的非法 UTF-8 字节常见于 Windows 提交的中文日志避免整个解析崩溃。这个图谱是后续「按需恢复任意 commit 版本」的基础。3. 运行实操从下载.git到生成可运行源码的完整链路GitHack-master 的运行流程高度自动化但每一步都有明确的输入输出契约。理解这个链路是避免「跑起来但恢复不全」的关键。整个过程分为四步环境准备 → 数据获取 → 对象解析 → 文件落地。下面以一个真实案例演示某静态站点暴露了http://example.com/.git/我们需在 3 分钟内拿到其src/目录下的全部 Vue 组件。3.1 环境准备与依赖确认GitHack-master 是纯 Python 项目仅依赖标准库Python 3.6无需pip install。但有两个隐性前提必须满足Python 版本必须 ≥3.6因使用f-string和pathlib。若系统默认是 Python 2.7需显式调用python3 githack.py ...zlib 支持Python 编译时需启用 zlib绝大多数发行版默认开启。验证方法python3 -c import zlib; print(zlib.__version__) # 输出类似 1.2.11 即正常注意某些 Alpine Linux 容器镜像精简版会移除 zlib报错ModuleNotFoundError: No module named zlib。此时需重装 Python 或换基础镜像如python:3.9-slim。3.2 数据获取download.py的智能路径补全与断点续传download.py模块负责从 URL 下载.git目录。它不简单wget -r而是基于 Git 目录结构做路径探测 并行下载首先请求/.git/HEAD确认.git目录存在且可读解析HEAD内容如ref: refs/heads/main再请求/.git/refs/heads/main获取当前 commit hash并行下载关键路径/objects/info/packs获取 packfile 列表、/objects/xx/按 hash 前两位并发下载如/objects/ab/,/objects/cd/对于大仓库若objects/下文件过多自动 fallback 到下载packfile/objects/pack/pack-*.pack并解包避免百万级小文件 HTTP 请求# 实际执行命令带详细日志 python3 githack.py \ -u http://example.com/.git/ \ -o ./recovered_example \ -f src/**/*.vue,package.json \ --no-commit \ -v # -v 开启详细日志看到每个 HTTP 请求和对象解析进度日志中你会看到类似[INFO] HEAD points to ref: refs/heads/main [INFO] Downloading ref file: http://example.com/.git/refs/heads/main [INFO] Found commit: a1b2c3d4e5f67890... [INFO] Starting parallel download of objects/ab/, objects/cd/, ... [INFO] Parsed 127 objects, 42 trees, 35 blobs (filtered: 18 vue files)3.3 文件落地recovery.py的树形展开与路径映射recovery.py是最终「写硬盘」的模块。它接收commit.py构建的图谱和objects.py解析的 blob 内容将 Git 的 tree 对象一种目录结构的序列化层层展开为真实文件路径。核心逻辑是递归解析 tree 对象# recovery.py 树展开伪代码简化 def expand_tree(tree_hash, objects_dir, base_path): 递归展开 tree 对象写入文件系统 tree 对象格式mode space filename \x00 sha1_binary 重复多次 tree_path get_object_path(objects_dir, tree_hash) _, tree_content parse_object_from_file(tree_path) i 0 while i len(tree_content): # 解析 mode 和 filename直到 \x00 null_pos tree_content.find(b\x00, i) if null_pos -1: break header tree_content[i:null_pos] i null_pos 1 # header 示例: b100644 README.md parts header.split(b , 1) if len(parts) 2: continue mode parts[0] filename parts[1] # 接下来 20 字节是 SHA-1 hash if i 20 len(tree_content): break blob_hash tree_content[i:i20].hex() i 20 full_path os.path.join(base_path, filename.decode(utf-8, errorsignore)) # 如果是 blob文件写入内容如果是 tree目录递归展开 if mode.startswith(b100): # 普通文件 blob_path get_object_path(objects_dir, blob_hash) _, blob_content parse_object_from_file(blob_path) if blob_content: os.makedirs(os.path.dirname(full_path), exist_okTrue) with open(full_path, wb) as f: f.write(blob_content) elif mode.startswith(b040): # tree子目录 expand_tree(blob_hash, objects_dir, full_path) # 调用入口从 commit 的 tree 字段开始 expand_tree(commit_graph[a1b2c3...][tree], ./downloaded_objects/, ./recovered_example/)这个递归函数的关键是它完全模拟了git checkout的路径映射逻辑但用 Python 实现可控性更强。例如当filename包含../时os.path.join会自动规范化为绝对路径从而天然防御路径遍历攻击../../../etc/passwd会被转为/etc/passwd而os.makedirs会因权限失败不会静默写入。4. 常见问题排查5 条血泪踩坑记录与解决方案在上百次真实环境恢复中以下问题出现频率最高。它们往往导致「程序不报错但恢复结果为空」或「部分文件缺失」属于典型的「玄学失败」。这里按「现象 → 原因 → 解决」给出可立即执行的方案。4.1 现象[WARN] Failed to parse .git/objects/ab/cdef...: Size mismatch大量刷屏最终recovered/为空原因目标.git/objects/目录被 CDN 或代理缓存截断部分对象文件只有几百字节但HEAD中声明的 size 是几 KB。parse_object_from_file的校验失败后跳过该对象若此对象是 root tree则整个恢复链路中断。解决添加--skip-size-check参数需修改源码见下文或手动下载完整.git目录用wget -r -np -nH --cut-dirs1 -R index.html* http://target.com/.git/。4.2 现象[INFO] Downloading ref file: ... 403 Forbidden但浏览器能打开/.git/HEAD原因服务器对/.git/refs/路径做了额外鉴权如 Nginx 的location ~ /\.git/refs/规则而HEAD因在根目录未被拦截。download.py默认尝试所有 refs遇到 403 就停止。解决改用--no-commit模式它只读HEAD和index不碰refs/或手动创建./local_refs/heads/main文件内容为a1b2c3...然后用-u ./local_refs/。4.3 现象恢复出的package.json里main: index.js但index.js文件不存在原因index.js在.gitignore中或从未被git add因此未进入对象数据库。GitHack 只能恢复 Git 知道的文件不能恢复工作区未跟踪的文件。解决检查./downloaded_objects/下是否有index.js对应的 blob用find . -name in*搜索若无则说明该文件确实未提交此时需结合其他线索如webpack.config.js中的entry配置推测路径。4.4 现象recovered/src/components/下全是空目录无.vue文件原因-f src/**/*.vue过滤参数中**是 shell 展开而非程序内 glob。若你在 Windows CMD 下运行**不被识别实际传给程序的是字面量src/**/*.vue导致匹配失败。解决Windows 用户改用 PowerShell 或 Git BashLinux/macOS 用户确保用单引号包裹-f src/**/*.vue防止 shell 提前展开。4.5 现象程序卡在[INFO] Parsing commit a1b2c3...超过 2 分钟CPU 占用 100%原因max_depth设得过大如-d 10且仓库有大量 merge commit 形成的稠密 DAGBFS 队列爆炸。queue中堆积数万节点visited集合查找变慢。解决立即CtrlC改用-d 2重新运行若需深历史先用git log --oneline | head -50估算活跃 commit 数再设depth为该数的 1.5 倍。5. 二次开发指南定制化恢复逻辑与企业级集成技巧GitHack-master 的设计初衷是「开箱即用」但实际工作中你常需要它做些超出默认能力的事比如只恢复某个特定分支的某次提交、把恢复结果自动推送到内部审计平台、或与 Jenkins 流水线联动在部署前自动扫描.git是否残留。这些需求无法靠参数解决必须修改代码。下面分享三个经过生产验证的改造方向附可直接粘贴的代码片段。5.1 场景一精准恢复指定分支的指定 commit非 HEAD默认逻辑只从HEAD出发但有时你需要origin/dev分支上abc123这个 commit 的代码比如该分支有未合并的敏感配置。改造点在commit.py的入口函数# 修改 commit.py 中的 build_commit_graph 函数签名和逻辑 def build_commit_graph(refs_dir, objects_dir, target_commitNone, max_depth3): 新增 target_commit 参数若提供则只从此 commit 开始构建图谱忽略 refs_dir graph {} visited set() queue deque() if target_commit: # 直接从指定 commit 开始不读 refs if len(target_commit) 40: queue.append((target_commit, 0)) else: raise ValueError(target_commit must be 40-char SHA-1 hash) else: # 原有逻辑从 refs 读取 for ref_path in get_all_ref_files(refs_dir): # ... 原有代码 # ... 后续 BFS 逻辑不变 return graph # 在 githack.py 中增加参数 parser.add_argument(--commit, helpSpecific commit hash to recover (bypasses refs)) # 调用时 graph build_commit_graph(args.refs_dir, args.objects_dir, target_commitargs.commit, max_depthargs.depth)这样调用即可python3 githack.py -u ./local_git/ --commit abc123def456... -o ./dev_branch_code/5.2 场景二恢复时自动计算文件哈希并写入审计日志安全合规要求记录恢复出的每个文件的 SHA-256用于后续比对。在recovery.py的expand_tree函数中blob_content写入前插入哈希计算# 在 recovery.py 的 expand_tree 函数内写入文件前 import hashlib if blob_content: # 计算 SHA-256 file_hash hashlib.sha256(blob_content).hexdigest() # 写入审计日志追加模式避免覆盖 with open(./recovery_audit.log, a) as log: log.write(f{full_path}\t{file_hash}\t{blob_hash}\n) # 原有写入逻辑 os.makedirs(os.path.dirname(full_path), exist_okTrue) with open(full_path, wb) as f: f.write(blob_content)提示审计日志用\t分隔方便awk -F\t {print $1,$2}提取路径和哈希。日志文件名硬编码为recovery_audit.log避免参数污染主逻辑。5.3 场景三与 CI/CD 集成在部署前自动检测并告警将 GitHack 作为 Jenkins Pipeline 的一个 stage一旦发现.git目录立即中止部署并通知 Slack。关键是要捕获「.git存在但不可读」这种边界情况// Jenkinsfile 片段 stage(Security Check: Git Leakage) { steps { script { // 检查 workspace 是否含 .git 且可读 def gitDir ${WORKSPACE}/.git if (sh(script: test -d ${gitDir} test -r ${gitDir}/HEAD, returnStatus: true) 0) { echo CRITICAL: .git directory found in workspace! Aborting deploy. // 调用 GitHack 生成报告 sh python3 githack.py -u ${gitDir} -o ./git_leak_report/ --no-commit -f * 2/dev/null || true // 发送 Slack 通知需配置 Slack 插件 slackSend channel: #alerts, message: Git leak detected in ${JOB_NAME}! Report: ${WORKSPACE}/git_leak_report/ error Git leakage detected. Deployment halted. } else { echo OK: No exposed .git directory found. } } } }这个 stage 的价值在于它把「人工安全检查」变成了「每次部署必走的流水线关卡」。从那以后我每次写 Jenkinsfile都强制走一遍sh test -d .git检查哪怕项目是前端静态页——因为太多人以为「没后端就不用管 Git 安全」结果 Vue 的api.js里硬编码了测试环境 token。希望帮到你。本文还有配套的精品资源点击获取