1. 从一个被忽略的目录说起:Git泄露到底在泄露什么很多人第一次接触Git泄露,是在CTF比赛或者授权渗透测试的靶场里。题目往往只给一个URL,打开一看是个普普通通的网站首页,源码里没有注释、没有隐藏链接、没有备份文件。这时候有经验的人会做一件事:在URL后面拼一个/.git/。如果服务器返回的不是404,而是目录列表或者某个文件的内容,那基本可以确定——这个站点的版本控制目录被直接暴露在了Web根目录下。Git泄露(.git leakage)的本质,是开发者在部署网站时,把整个项目目录(包括隐藏的.git文件夹)一起上传到了Web服务器。.git目录里保存着这个项目从初始化到当前的所有版本信息:提交历史、分支、标签、暂存区、甚至开发者本地未提交的储藏(Stash)。攻击者或者CTF选手拿到这个目录,就等于拿到了项目的"时间机器",可以还原出源码、找到被删除的敏感文件、读取提交日志里的密码和密钥。这篇文章面向三类人:正在打CTFHub Git泄露系列题目的选手、做Web安全入门学习的安全爱好者、以及需要检查自己项目部署是否规范的开发者。我会从git log和git stash这两个最容易被忽视的泄露点切入,把CTFHub整个Git泄露系列的解题思路完整拆一遍,同时补充大量实战中才会遇到的细节和坑。先明确一个概念:.git目录不是普通文件夹,它有一套固定的内部结构。核心文件包括HEAD(指向当前分支)、config(仓库配置)、index(暂存区索引)、logs/HEAD(引用日志)、objects/(所有Git对象的存储目录)。理解这套结构,是后面所有操作的基础。提示:本文所有操作均在授权靶场或自己搭建的本地环境中进行。未经授权对他人站点进行目录探测和源码还原属于违规行为。2. .git目录的内部构造与还原逻辑2.1 为什么光有目录列表还不够很多人以为只要/.git/能访问就万事大吉,实际上服务器配置千差万别。有的服务器开启了目录浏览,你能直接看到objects、refs、logs等子目录;有的服务器关闭了目录浏览,但允许直接访问具体文件路径,比如/.git/HEAD能返回内容,/.git/objects/却返回403。这两种情况对应的还原策略完全不同。还有一种更隐蔽的情况:服务器对.git路径做了重写或者拦截,返回一个自定义的404页面,状态码却是200。这时候如果只看状态码会误判,必须看响应体的实际内容。我在靶场里就遇到过返回"页面不存在"但状态码200的情况,差点以为是死路。判断.git是否真实存在,最可靠的方法是请求/.git/HEAD。正常的Git仓库,这个文件的内容格式是ref: refs/heads/master或者ref: refs/heads/main。如果返回的是这个格式,说明目录真实存在且可读。2.2 Git对象存储:松散对象与打包对象.git/objects/目录下存放的是Git的所有数据。Git对象分四种类型:blob(文件内容)、tree(目录结构)、commit(提交记录)、tag(标签)。每个对象都有一个40位的SHA-1哈希值作为唯一标识。对象在磁盘上有两种存储形式。一种是松散对象(loose object),路径规则是:哈希值前两位作为子目录名,后38位作为文件名。比如哈希a1b2c3...对应的路径是.git/objects/a1/b2c3...。另一种是打包对象(packed object),存放在.git/objects/pack/目录下,文件名形如pack-xxxx.pack和pack-xxxx.idx。CTF题目里,如果服务器允许目录浏览,你能直接看到objects下的所有松散对象文件,那就好办——写个脚本把所有对象下载下来,按原路径放回本地,然后git checkout就能还原。但如果对象被打包了,或者服务器不允许列目录,就需要另一套工具。2.3 常用还原工具的能力边界目前社区里常用的Git泄露利用工具有几款,各有适用场景:工具核心能力适用场景局限GitHack自动爬取objects并还原目录可浏览、松散对象对packed对象支持弱dvcs-ripper支持多种版本控制泄露通用场景配置稍复杂git-dumper递归下载.git目录目录可浏览需要Python环境手工脚本完全可控特殊路径、自定义逻辑需要自己写我个人的习惯是:先用git-dumper跑一遍,它能自动识别目录结构并递归下载。如果跑不通,再手工分析。git-dumper的用法很简单:pip install git-dumper git-dumper http://target/.git/ ./output_dir跑完之后进入output_dir,执行git log看提交历史,git status看工作区状态,git stash list看储藏列表。这三条命令基本能覆盖CTFHub Git泄露系列的大部分考点。注意:git-dumper下载的是.git目录本身,不是工作区文件。下载完成后需要执行git checkout .或者git reset --hard才能把文件还原到工作目录。3. git log:从提交历史里挖出被删除的flag3.1 提交历史为什么是信息金矿git log展示的是仓库的提交记录,每条记录包含提交哈希、作者、日期、提交信息。在CTF场景里,出题人经常把flag放在某次提交里,然后在下一次提交中删除。如果你只看当前工作区的文件,flag已经没了;但git log能看到所有历史提交,git show能查看任意一次提交的完整内容。CTFHub的Git泄露系列里,Log这道题就是典型。题目给一个站点,.git目录可访问。用工具还原后,执行git log会看到类似这样的输出:commit 8a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b Author: developer <dev@example.com> Date: Mon Jan 1 12:00:00 2024 +0800 remove flag commit 1f2e3d4c5b6a7f8e9d0c1b2a3f4e5d6c7b8a9f0e Author: developer <dev@example.com> Date: Mon Jan 1 11:59:00 2024 +0800 add flag看到"remove flag"和"add flag"这两条提交,答案就很明显了。执行git show 1f2e3d4c(前一次提交的哈希),就能看到那次提交里flag文件的内容。3.2 还原后git log看不到东西怎么办这是实战中最常见的问题。工具跑完了,目录里也有.git,但git log报错说"not a git repository"或者"your current branch does not have any commits yet"。原因通常有三种:第一种,HEAD文件下载失败或者内容不对。检查.git/HEAD是否存在,内容是否是ref: refs/heads/master。如果HEAD指向的分支在refs/heads/下不存在,Git就找不到提交起点。第二种,objects目录不完整。松散对象下载时可能因为网络问题漏了几个,导致Git无法解析提交链。这时候可以尝试git fsck --lost-found,它会扫描所有对象并尝试恢复。第三种,仓库是空的,提交信息在logs/HEAD里。有些题目会把提交记录放在引用日志里,而不是正常的commit对象。logs/HEAD记录的是HEAD的移动历史,格式是"旧哈希 新哈希 作者 时间 操作"。即使commit对象丢失,从日志里也能看到提交哈希,再结合objects目录里的对象,有可能手工拼出提交内容。3.3 用git show和git diff定位flag的实操假设git log已经能看到提交历史,接下来就是定位flag。常用的命令组合:# 查看所有提交的简要信息 git log --oneline # 查看某次提交的完整改动 git show <commit-hash> # 查看两次提交之间的差异 git diff <commit-hash-1> <commit-hash-2> # 查看所有分支的提交历史 git log --all --oneline # 查看被删除的文件在哪个提交里还存在 git log --all --diff-filter=D --name-onlygit log --all这个参数特别重要。有时候flag不在当前分支上,而在另一个分支或者一个悬空的提交里。--all会展示所有引用能到达的提交,包括远程分支和标签。还有一个技巧:如果提交信息里没有明显的"flag"字样,可以用git log -p直接看每次提交的补丁内容,配合grep过滤:git log -p | grep -i "flag\|ctfhub\|key"我在做这类题时,习惯先把所有提交的哈希列出来,然后逐个git show,同时用git diff对比相邻提交。这样即使出题人把flag拆散在多次提交里,也能拼出来。4. git stash:藏在储藏区里的"临时保存"4.1 Stash的工作原理与泄露风险git stash是开发者常用的功能:当你正在改代码,突然需要切换到另一个分支处理紧急问题,但又不想提交半成品,就可以用git stash把当前工作区的改动"储藏"起来。储藏的内容包括暂存区和工作区的修改,保存成一个特殊的提交对象,然后工作区恢复到干净状态。问题在于,git stash保存的内容同样存放在.git目录里。具体位置是.git/refs/stash(引用文件)和.git/logs/refs/stash(引用日志)。储藏的内容本身以commit对象的形式存在objects目录里。如果.git目录泄露,攻击者执行git stash list就能看到所有储藏记录,git stash show和git stash pop能查看和恢复储藏的内容。CTFHub的Stash题目就是利用这一点。出题人把flag放在一次stash操作里,然后可能又做了其他提交,让当前工作区看起来很正常。但git stash list会暴露储藏记录。4.2 还原stash的完整操作链路假设已经用工具还原了.git目录,进入目录后:# 查看储藏列表 git stash list # 输出示例: # stash@{0}: WIP on master: 1f2e3d4 add flag # 查看某个储藏的改动内容 git stash show -p stash@{0} # 恢复储藏内容到工作区 git stash pop stash@{0} # 或者只查看不恢复 git stash apply stash@{0}git stash show -p会以补丁形式展示储藏的具体改动,flag通常就在这里面。如果git stash list是空的,但你知道应该有储藏,可以检查.git/logs/refs/stash文件是否存在。这个文件记录了stash的创建历史,即使refs/stash引用被删除,日志里可能还有线索。还有一种情况:储藏对象存在,但引用被删了。这时候git stash list看不到,但git fsck --unreachable能列出所有不可达的对象,其中可能包含储藏提交。找到哈希后,用git show <hash>查看内容。4.3 stash与log的组合考点CTFHub系列里有些题目会把log和stash结合起来考。比如:当前分支的提交历史里flag被删了,但储藏区里还留着包含flag的版本。或者反过来,储藏区是空的,但某个悬空提交里有flag。处理这类组合题,我的排查顺序是:先git log --all --oneline看所有提交再git stash list看储藏然后git fsck --lost-found看悬空对象最后检查.git/logs/下的所有日志文件这个顺序的逻辑是:从最明显的线索开始,逐步深入到隐藏的角落。大部分题目在前两步就能解决,少数需要走到第三步。提示:git fsck --lost-found会把悬空对象输出到.git/lost-found/目录下,commit对象放在commit/子目录,其他对象放在other/子目录。可以直接查看这些文件的内容。5. CTFHub Git泄露系列题目的通用解题框架5.1 信息收集阶段的判断清单拿到一个Git泄露题目,不要急着上工具。先做一轮快速判断,能省很多时间:访问/.git/HEAD,确认目录是否存在且可读访问/.git/config,看仓库配置,有时能看到远程地址和分支信息访问/.git/logs/HEAD,看引用日志,能直接看到提交哈希尝试/.git/objects/,判断是否允许目录浏览检查/.git/refs/heads/和/.git/refs/stash,看分支和储藏引用这五步做完,基本能确定用哪种还原策略。如果目录可浏览,直接上git-dumper;如果只能访问特定文件,就需要手工构造请求,逐个下载关键文件。5.2 不同泄露程度的应对策略根据服务器暴露的程度,我把Git泄露分成三个等级:一级:完全暴露。/.git/目录可浏览,所有文件都能下载。直接用git-dumper或GitHack一键还原,然后git log、git stash list、git diff三板斧。二级:部分暴露。目录不可浏览,但关键文件(HEAD、config、objects下的松散对象)可访问。需要先下载HEAD确定分支,再下载refs/heads/master获取最新提交哈希,然后根据哈希递归下载commit对象和tree对象,最后还原blob。这个过程可以写脚本自动化,也可以借助git-dumper的部分功能。三级:仅日志暴露。只能访问/.git/logs/HEAD,看不到objects。这种情况下,日志里能拿到提交哈希,但拿不到提交内容。如果题目只考"找到提交哈希"或者"从日志里读flag",那直接看日志就行。CTFHub里有一道题就是把flag直接写在日志的提交信息里。5.3 还原后的验证与flag提取还原完成后,不要只盯着git log。我习惯做一轮全面检查:# 查看当前工作区所有文件 ls -la # 查看所有分支 git branch -a # 查看所有标签 git tag # 查看所有引用 git show-ref # 查看引用日志 cat .git/logs/HEAD # 搜索所有对象里的flag关键词 git rev-list --all --objects | cut -d' ' -f1 | xargs -I{} git cat-file -p {} 2>/dev/null | grep -i "flag\|ctfhub"最后那条命令会遍历所有Git对象,把内容打印出来并搜索关键词。虽然慢,但覆盖最全,适合前面几步都没找到flag的情况。6. 实战中容易踩的坑与排查经验6.1 工具跑完但git命令报错的排查最常见的问题是git-dumper跑完了,进入目录执行git log却报"fatal: your current branch 'master' does not have any commits yet"。这通常是因为objects目录下载不完整,或者refs/heads/master文件缺失。排查步骤:检查.git/refs/heads/master是否存在,内容是否是一个40位哈希检查.git/objects/下是否有对应的对象文件(前两位作为目录名)如果对象是packed形式,检查.git/objects/pack/下是否有.pack和.idx文件执行git fsck --full看具体报什么错如果refs/heads/master缺失但logs/HEAD存在,可以从日志里提取最新的提交哈希,手工写入refs/heads/master:# 从logs/HEAD提取最新哈希 tail -1 .git/logs/HEAD | cut -d' ' -f2 > .git/refs/heads/master然后重新执行git log。6.2 对象哈希对不上与pack文件处理有时候git fsck会报"missing blob"或者"broken link",说明某些对象没下载全。如果对象是松散形式,可以根据报错里的哈希值,构造URL重新下载:/.git/objects/ab/cdef...。如果对象是packed形式,处理起来麻烦一些。.pack文件是二进制格式,包含了多个对象的压缩数据;.idx文件是索引,记录了每个对象在pack文件中的偏移量。要还原pack里的对象,需要把.pack和.idx都下载下来,放到.git/objects/pack/目录,然后执行:git index-pack .git/objects/pack/pack-xxxx.pack或者直接用git verify-pack查看pack内容。如果.idx文件缺失,可以用git index-pack从.pack文件重建索引。我在一次靶场练习中遇到过只有.pack没有.idx的情况,当时用git index-pack重建索引后成功还原了所有对象。这个命令会读取pack文件,重新计算索引并生成.idx文件。6.3 服务器返回200但内容不对的识别有些服务器配置了URL重写,把所有不存在的路径都重定向到首页,返回200状态码。这时候请求/.git/HEAD会返回首页的HTML,而不是Git的HEAD内容。识别方法是看响应体的特征:Git的HEAD文件内容很短,格式固定为ref: refs/heads/xxx;而HTML首页通常有<html>、<head>等标签。还有一种情况是服务器返回了自定义的404页面,但状态码是200。这时候不能只看状态码,要看内容长度和内容特征。我通常会用curl -I看响应头,再用curl看响应体前几百个字符,综合判断。# 只看响应头 curl -I http://target/.git/HEAD # 看响应体前200字符 curl http://target/.git/HEAD | head -c 200如果响应头里的Content-Type是text/html而不是text/plain,基本可以判断返回的不是Git文件。6.4 关于授权与合规的提醒最后必须强调一点:Git泄露利用技术只应该在授权环境下使用。CTF靶场、自己搭建的测试环境、明确授权的渗透测试项目,这些都没问题。但未经授权对真实站点进行目录探测、源码下载、数据还原,属于违法行为,后果严重。作为开发者,也应该检查自己的部署流程:确保.git目录不会被上传到Web根目录,在Nginx或Apache配置里加上对.git路径的拒绝规则,使用CI/CD工具自动部署而不是手工上传整个项目目录。这些措施能从源头上避免Git泄露。# Nginx拒绝访问.git目录的配置示例 location ~ /\.git { deny all; return 404; }# Apache拒绝访问.git目录的配置示例 <DirectoryMatch "^/.*/\.git/"> Require all denied </DirectoryMatch>这两段配置可以直接抄到自己的服务器配置里,能挡住绝大多数自动化扫描工具。7. 从CTF到真实场景:Git泄露的延伸思考CTFHub的Git泄露系列题目设计得很巧妙,把log、stash、index、HEAD等不同泄露点拆成独立题目,让选手逐个掌握。但真实场景里的Git泄露往往更复杂:可能同时存在多个泄露点,可能对象被打包且索引缺失,可能服务器只暴露了部分日志文件。我的建议是,做完CTFHub系列后,自己搭一个本地Git仓库,模拟各种泄露场景:删除分支后看悬空对象、创建stash后删除引用、打包对象后删除索引文件。把这些场景都跑一遍,比单纯刷题收获大得多。另外,git log和git stash只是Git泄露的冰山一角。.git/config里可能藏着远程仓库地址和凭证,.git/index里可能包含未提交的文件路径,.git/COMMIT_EDITMSG里可能有上一次提交的完整信息。这些文件在特定场景下都可能成为信息泄露的入口。我在实际检查自己项目部署时,养成了一个习惯:部署完成后,用浏览器访问/.git/HEAD、/.git/config、/.git/logs/HEAD这几个路径,确认返回404。这个简单的自检动作,能避免很多低级但危险的问题。毕竟,Git泄露的代价不是丢一个flag那么简单,而是整个项目的源码和历史都可能被完整还原。
1. 从一个被忽略的目录说起:Git泄露到底在泄露什么很多人第一次接触Git泄露,是在CTF比赛或者授权渗透测试的靶场里。题目往往只给一个URL,打开一看是个普普通通的网站首页,源码里没有注释、没有隐藏链接、没有备份文件。这时候有经验的人会做一件事:在URL后面拼一个/.git/。如果服务器返回的不是404,而是目录列表或者某个文件的内容,那基本可以确定——这个站点的版本控制目录被直接暴露在了Web根目录下。Git泄露(.git leakage)的本质,是开发者在部署网站时,把整个项目目录(包括隐藏的.git文件夹)一起上传到了Web服务器。.git目录里保存着这个项目从初始化到当前的所有版本信息:提交历史、分支、标签、暂存区、甚至开发者本地未提交的储藏(Stash)。攻击者或者CTF选手拿到这个目录,就等于拿到了项目的"时间机器",可以还原出源码、找到被删除的敏感文件、读取提交日志里的密码和密钥。这篇文章面向三类人:正在打CTFHub Git泄露系列题目的选手、做Web安全入门学习的安全爱好者、以及需要检查自己项目部署是否规范的开发者。我会从git log和git stash这两个最容易被忽视的泄露点切入,把CTFHub整个Git泄露系列的解题思路完整拆一遍,同时补充大量实战中才会遇到的细节和坑。先明确一个概念:.git目录不是普通文件夹,它有一套固定的内部结构。核心文件包括HEAD(指向当前分支)、config(仓库配置)、index(暂存区索引)、logs/HEAD(引用日志)、objects/(所有Git对象的存储目录)。理解这套结构,是后面所有操作的基础。提示:本文所有操作均在授权靶场或自己搭建的本地环境中进行。未经授权对他人站点进行目录探测和源码还原属于违规行为。2. .git目录的内部构造与还原逻辑2.1 为什么光有目录列表还不够很多人以为只要/.git/能访问就万事大吉,实际上服务器配置千差万别。有的服务器开启了目录浏览,你能直接看到objects、refs、logs等子目录;有的服务器关闭了目录浏览,但允许直接访问具体文件路径,比如/.git/HEAD能返回内容,/.git/objects/却返回403。这两种情况对应的还原策略完全不同。还有一种更隐蔽的情况:服务器对.git路径做了重写或者拦截,返回一个自定义的404页面,状态码却是200。这时候如果只看状态码会误判,必须看响应体的实际内容。我在靶场里就遇到过返回"页面不存在"但状态码200的情况,差点以为是死路。判断.git是否真实存在,最可靠的方法是请求/.git/HEAD。正常的Git仓库,这个文件的内容格式是ref: refs/heads/master或者ref: refs/heads/main。如果返回的是这个格式,说明目录真实存在且可读。2.2 Git对象存储:松散对象与打包对象.git/objects/目录下存放的是Git的所有数据。Git对象分四种类型:blob(文件内容)、tree(目录结构)、commit(提交记录)、tag(标签)。每个对象都有一个40位的SHA-1哈希值作为唯一标识。对象在磁盘上有两种存储形式。一种是松散对象(loose object),路径规则是:哈希值前两位作为子目录名,后38位作为文件名。比如哈希a1b2c3...对应的路径是.git/objects/a1/b2c3...。另一种是打包对象(packed object),存放在.git/objects/pack/目录下,文件名形如pack-xxxx.pack和pack-xxxx.idx。CTF题目里,如果服务器允许目录浏览,你能直接看到objects下的所有松散对象文件,那就好办——写个脚本把所有对象下载下来,按原路径放回本地,然后git checkout就能还原。但如果对象被打包了,或者服务器不允许列目录,就需要另一套工具。2.3 常用还原工具的能力边界目前社区里常用的Git泄露利用工具有几款,各有适用场景:工具核心能力适用场景局限GitHack自动爬取objects并还原目录可浏览、松散对象对packed对象支持弱dvcs-ripper支持多种版本控制泄露通用场景配置稍复杂git-dumper递归下载.git目录目录可浏览需要Python环境手工脚本完全可控特殊路径、自定义逻辑需要自己写我个人的习惯是:先用git-dumper跑一遍,它能自动识别目录结构并递归下载。如果跑不通,再手工分析。git-dumper的用法很简单:pip install git-dumper git-dumper http://target/.git/ ./output_dir跑完之后进入output_dir,执行git log看提交历史,git status看工作区状态,git stash list看储藏列表。这三条命令基本能覆盖CTFHub Git泄露系列的大部分考点。注意:git-dumper下载的是.git目录本身,不是工作区文件。下载完成后需要执行git checkout .或者git reset --hard才能把文件还原到工作目录。3. git log:从提交历史里挖出被删除的flag3.1 提交历史为什么是信息金矿git log展示的是仓库的提交记录,每条记录包含提交哈希、作者、日期、提交信息。在CTF场景里,出题人经常把flag放在某次提交里,然后在下一次提交中删除。如果你只看当前工作区的文件,flag已经没了;但git log能看到所有历史提交,git show能查看任意一次提交的完整内容。CTFHub的Git泄露系列里,Log这道题就是典型。题目给一个站点,.git目录可访问。用工具还原后,执行git log会看到类似这样的输出:commit 8a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b Author: developer <dev@example.com> Date: Mon Jan 1 12:00:00 2024 +0800 remove flag commit 1f2e3d4c5b6a7f8e9d0c1b2a3f4e5d6c7b8a9f0e Author: developer <dev@example.com> Date: Mon Jan 1 11:59:00 2024 +0800 add flag看到"remove flag"和"add flag"这两条提交,答案就很明显了。执行git show 1f2e3d4c(前一次提交的哈希),就能看到那次提交里flag文件的内容。3.2 还原后git log看不到东西怎么办这是实战中最常见的问题。工具跑完了,目录里也有.git,但git log报错说"not a git repository"或者"your current branch does not have any commits yet"。原因通常有三种:第一种,HEAD文件下载失败或者内容不对。检查.git/HEAD是否存在,内容是否是ref: refs/heads/master。如果HEAD指向的分支在refs/heads/下不存在,Git就找不到提交起点。第二种,objects目录不完整。松散对象下载时可能因为网络问题漏了几个,导致Git无法解析提交链。这时候可以尝试git fsck --lost-found,它会扫描所有对象并尝试恢复。第三种,仓库是空的,提交信息在logs/HEAD里。有些题目会把提交记录放在引用日志里,而不是正常的commit对象。logs/HEAD记录的是HEAD的移动历史,格式是"旧哈希 新哈希 作者 时间 操作"。即使commit对象丢失,从日志里也能看到提交哈希,再结合objects目录里的对象,有可能手工拼出提交内容。3.3 用git show和git diff定位flag的实操假设git log已经能看到提交历史,接下来就是定位flag。常用的命令组合:# 查看所有提交的简要信息 git log --oneline # 查看某次提交的完整改动 git show <commit-hash> # 查看两次提交之间的差异 git diff <commit-hash-1> <commit-hash-2> # 查看所有分支的提交历史 git log --all --oneline # 查看被删除的文件在哪个提交里还存在 git log --all --diff-filter=D --name-onlygit log --all这个参数特别重要。有时候flag不在当前分支上,而在另一个分支或者一个悬空的提交里。--all会展示所有引用能到达的提交,包括远程分支和标签。还有一个技巧:如果提交信息里没有明显的"flag"字样,可以用git log -p直接看每次提交的补丁内容,配合grep过滤:git log -p | grep -i "flag\|ctfhub\|key"我在做这类题时,习惯先把所有提交的哈希列出来,然后逐个git show,同时用git diff对比相邻提交。这样即使出题人把flag拆散在多次提交里,也能拼出来。4. git stash:藏在储藏区里的"临时保存"4.1 Stash的工作原理与泄露风险git stash是开发者常用的功能:当你正在改代码,突然需要切换到另一个分支处理紧急问题,但又不想提交半成品,就可以用git stash把当前工作区的改动"储藏"起来。储藏的内容包括暂存区和工作区的修改,保存成一个特殊的提交对象,然后工作区恢复到干净状态。问题在于,git stash保存的内容同样存放在.git目录里。具体位置是.git/refs/stash(引用文件)和.git/logs/refs/stash(引用日志)。储藏的内容本身以commit对象的形式存在objects目录里。如果.git目录泄露,攻击者执行git stash list就能看到所有储藏记录,git stash show和git stash pop能查看和恢复储藏的内容。CTFHub的Stash题目就是利用这一点。出题人把flag放在一次stash操作里,然后可能又做了其他提交,让当前工作区看起来很正常。但git stash list会暴露储藏记录。4.2 还原stash的完整操作链路假设已经用工具还原了.git目录,进入目录后:# 查看储藏列表 git stash list # 输出示例: # stash@{0}: WIP on master: 1f2e3d4 add flag # 查看某个储藏的改动内容 git stash show -p stash@{0} # 恢复储藏内容到工作区 git stash pop stash@{0} # 或者只查看不恢复 git stash apply stash@{0}git stash show -p会以补丁形式展示储藏的具体改动,flag通常就在这里面。如果git stash list是空的,但你知道应该有储藏,可以检查.git/logs/refs/stash文件是否存在。这个文件记录了stash的创建历史,即使refs/stash引用被删除,日志里可能还有线索。还有一种情况:储藏对象存在,但引用被删了。这时候git stash list看不到,但git fsck --unreachable能列出所有不可达的对象,其中可能包含储藏提交。找到哈希后,用git show <hash>查看内容。4.3 stash与log的组合考点CTFHub系列里有些题目会把log和stash结合起来考。比如:当前分支的提交历史里flag被删了,但储藏区里还留着包含flag的版本。或者反过来,储藏区是空的,但某个悬空提交里有flag。处理这类组合题,我的排查顺序是:先git log --all --oneline看所有提交再git stash list看储藏然后git fsck --lost-found看悬空对象最后检查.git/logs/下的所有日志文件这个顺序的逻辑是:从最明显的线索开始,逐步深入到隐藏的角落。大部分题目在前两步就能解决,少数需要走到第三步。提示:git fsck --lost-found会把悬空对象输出到.git/lost-found/目录下,commit对象放在commit/子目录,其他对象放在other/子目录。可以直接查看这些文件的内容。5. CTFHub Git泄露系列题目的通用解题框架5.1 信息收集阶段的判断清单拿到一个Git泄露题目,不要急着上工具。先做一轮快速判断,能省很多时间:访问/.git/HEAD,确认目录是否存在且可读访问/.git/config,看仓库配置,有时能看到远程地址和分支信息访问/.git/logs/HEAD,看引用日志,能直接看到提交哈希尝试/.git/objects/,判断是否允许目录浏览检查/.git/refs/heads/和/.git/refs/stash,看分支和储藏引用这五步做完,基本能确定用哪种还原策略。如果目录可浏览,直接上git-dumper;如果只能访问特定文件,就需要手工构造请求,逐个下载关键文件。5.2 不同泄露程度的应对策略根据服务器暴露的程度,我把Git泄露分成三个等级:一级:完全暴露。/.git/目录可浏览,所有文件都能下载。直接用git-dumper或GitHack一键还原,然后git log、git stash list、git diff三板斧。二级:部分暴露。目录不可浏览,但关键文件(HEAD、config、objects下的松散对象)可访问。需要先下载HEAD确定分支,再下载refs/heads/master获取最新提交哈希,然后根据哈希递归下载commit对象和tree对象,最后还原blob。这个过程可以写脚本自动化,也可以借助git-dumper的部分功能。三级:仅日志暴露。只能访问/.git/logs/HEAD,看不到objects。这种情况下,日志里能拿到提交哈希,但拿不到提交内容。如果题目只考"找到提交哈希"或者"从日志里读flag",那直接看日志就行。CTFHub里有一道题就是把flag直接写在日志的提交信息里。5.3 还原后的验证与flag提取还原完成后,不要只盯着git log。我习惯做一轮全面检查:# 查看当前工作区所有文件 ls -la # 查看所有分支 git branch -a # 查看所有标签 git tag # 查看所有引用 git show-ref # 查看引用日志 cat .git/logs/HEAD # 搜索所有对象里的flag关键词 git rev-list --all --objects | cut -d' ' -f1 | xargs -I{} git cat-file -p {} 2>/dev/null | grep -i "flag\|ctfhub"最后那条命令会遍历所有Git对象,把内容打印出来并搜索关键词。虽然慢,但覆盖最全,适合前面几步都没找到flag的情况。6. 实战中容易踩的坑与排查经验6.1 工具跑完但git命令报错的排查最常见的问题是git-dumper跑完了,进入目录执行git log却报"fatal: your current branch 'master' does not have any commits yet"。这通常是因为objects目录下载不完整,或者refs/heads/master文件缺失。排查步骤:检查.git/refs/heads/master是否存在,内容是否是一个40位哈希检查.git/objects/下是否有对应的对象文件(前两位作为目录名)如果对象是packed形式,检查.git/objects/pack/下是否有.pack和.idx文件执行git fsck --full看具体报什么错如果refs/heads/master缺失但logs/HEAD存在,可以从日志里提取最新的提交哈希,手工写入refs/heads/master:# 从logs/HEAD提取最新哈希 tail -1 .git/logs/HEAD | cut -d' ' -f2 > .git/refs/heads/master然后重新执行git log。6.2 对象哈希对不上与pack文件处理有时候git fsck会报"missing blob"或者"broken link",说明某些对象没下载全。如果对象是松散形式,可以根据报错里的哈希值,构造URL重新下载:/.git/objects/ab/cdef...。如果对象是packed形式,处理起来麻烦一些。.pack文件是二进制格式,包含了多个对象的压缩数据;.idx文件是索引,记录了每个对象在pack文件中的偏移量。要还原pack里的对象,需要把.pack和.idx都下载下来,放到.git/objects/pack/目录,然后执行:git index-pack .git/objects/pack/pack-xxxx.pack或者直接用git verify-pack查看pack内容。如果.idx文件缺失,可以用git index-pack从.pack文件重建索引。我在一次靶场练习中遇到过只有.pack没有.idx的情况,当时用git index-pack重建索引后成功还原了所有对象。这个命令会读取pack文件,重新计算索引并生成.idx文件。6.3 服务器返回200但内容不对的识别有些服务器配置了URL重写,把所有不存在的路径都重定向到首页,返回200状态码。这时候请求/.git/HEAD会返回首页的HTML,而不是Git的HEAD内容。识别方法是看响应体的特征:Git的HEAD文件内容很短,格式固定为ref: refs/heads/xxx;而HTML首页通常有<html>、<head>等标签。还有一种情况是服务器返回了自定义的404页面,但状态码是200。这时候不能只看状态码,要看内容长度和内容特征。我通常会用curl -I看响应头,再用curl看响应体前几百个字符,综合判断。# 只看响应头 curl -I http://target/.git/HEAD # 看响应体前200字符 curl http://target/.git/HEAD | head -c 200如果响应头里的Content-Type是text/html而不是text/plain,基本可以判断返回的不是Git文件。6.4 关于授权与合规的提醒最后必须强调一点:Git泄露利用技术只应该在授权环境下使用。CTF靶场、自己搭建的测试环境、明确授权的渗透测试项目,这些都没问题。但未经授权对真实站点进行目录探测、源码下载、数据还原,属于违法行为,后果严重。作为开发者,也应该检查自己的部署流程:确保.git目录不会被上传到Web根目录,在Nginx或Apache配置里加上对.git路径的拒绝规则,使用CI/CD工具自动部署而不是手工上传整个项目目录。这些措施能从源头上避免Git泄露。# Nginx拒绝访问.git目录的配置示例 location ~ /\.git { deny all; return 404; }# Apache拒绝访问.git目录的配置示例 <DirectoryMatch "^/.*/\.git/"> Require all denied </DirectoryMatch>这两段配置可以直接抄到自己的服务器配置里,能挡住绝大多数自动化扫描工具。7. 从CTF到真实场景:Git泄露的延伸思考CTFHub的Git泄露系列题目设计得很巧妙,把log、stash、index、HEAD等不同泄露点拆成独立题目,让选手逐个掌握。但真实场景里的Git泄露往往更复杂:可能同时存在多个泄露点,可能对象被打包且索引缺失,可能服务器只暴露了部分日志文件。我的建议是,做完CTFHub系列后,自己搭一个本地Git仓库,模拟各种泄露场景:删除分支后看悬空对象、创建stash后删除引用、打包对象后删除索引文件。把这些场景都跑一遍,比单纯刷题收获大得多。另外,git log和git stash只是Git泄露的冰山一角。.git/config里可能藏着远程仓库地址和凭证,.git/index里可能包含未提交的文件路径,.git/COMMIT_EDITMSG里可能有上一次提交的完整信息。这些文件在特定场景下都可能成为信息泄露的入口。我在实际检查自己项目部署时,养成了一个习惯:部署完成后,用浏览器访问/.git/HEAD、/.git/config、/.git/logs/HEAD这几个路径,确认返回404。这个简单的自检动作,能避免很多低级但危险的问题。毕竟,Git泄露的代价不是丢一个flag那么简单,而是整个项目的源码和历史都可能被完整还原。