扫码领资料
获网安教程
来Track安全社区投稿~
赢千元稿费!还有保底奖励~(https://bbs.zkaq.cn)
在漏洞赏金领域,很多研究人员都会寻找泄露的密钥,通常是扫描 GitHub 仓库中暴露的凭证。这种方法并不新颖,但我想尝试另一个角度——从已删除的文件中恢复密钥。开发者们常常忘记,Git 的历史记录会保留所有内容,即使文件已经从工作目录中删除了。
为此,我扫描了成千上万个来自上千家公司的仓库,寻找过往提交中隐藏的敏感信息。结果令人震惊——我发现了大量删除文件中包含的 API 令牌、凭证,甚至还有未吊销的活跃会话令牌。报告这些发现不仅帮助了受影响的公司提升安全性,最终我也通过漏洞赏金获得了总计 64,350 $的奖励。
在这篇博客中,我会描述我从收集 GitHub 仓库、搭建自动化程序,到发现并报告泄露信息的整个过程。
Git 内部机制
Git 是一个分布式版本控制系统,能够追踪文件变化并支持多名开发者协作开发。它维护了所有变更的完整历史记录,使用户可以回滚到之前的状态、基于某一版本创建分支开发新功能,并高效地合并更改。Git 的核心是一个基于内容寻址的文件系统,每个文件版本都会作为一个独特的对象存储在仓库中。
Git 跟踪的所有内容(文件、文件夹、提交等)都以对象的形式存储,并通过其 SHA-1 或 SHA-256 哈希值(取决于配置)进行标识。
Git 中有四种对象类型:
-
• Blob → 文件内容 -
• Tree → 目录结构 -
• Commit → 快照 + 元数据 -
• Tag → 注释标签对象
Git 的 Blob 与 Pack
一个 blob(二进制大对象)是 Git 存储文件内容的方式,不包含文件名或路径信息。当 Git 第一次存储一个对象时,它会以“松散对象(loose object)”的形式写入,如下所示:
.git/objects/ab/cdef1234567890...
其中 ab 是 SHA 的前两个十六进制字符,cdef1234567890… 是剩余部分。内部存储的二进制数据是经过 zlib 压缩的,代表一个单独的文件。
为了优化存储和性能,Git 会定期(默认当松散对象数量 ≥ 6700 时)将多个松散对象压缩成一个 pack 文件:
.git/objects/pack/pack-<hash>.pack
.pack 文件结构复杂且具有非常有趣的结构。幸运的是,我们并不需要完全理解它是如何构建的,因为我们可以使用 git-unpack-objects 来解包任何 .pack 文件。
如何识别 pack 文件和 blob 文件:50 41 43 4B — PACK | 78 01 — zlib 压缩的 blob 开始标志(图片使用 HexShare 获取)
有时在我们的 git 仓库中会产生未引用对象(也称悬挂对象)。这些是有效但未被引用的对象(提交、blob、树或标签),它们保留在 .git/objects/
目录中,但不再能通过任何分支、标签、暂存区或 reflog 访问。通常在重写历史(例如执行 git commit --amend
、rebase
、reset
或删除分支)时,会留下旧对象。尽管它们不再属于活跃历史,Git 仍会临时保留它们(默认两周),以便可能的恢复。你可以使用 git fsck --dangling
找到它们。
Git 提交历史
Git 中的每一次提交代表了仓库在某一时间点的快照。提交是不可变的,通过 SHA-1/SHA-256 哈希进行标识。它们存储以下内容:
-
• 指向树对象的引用,该树对象表示该次提交时的文件结构。 -
• 指向父提交的指针,形成一个有向无环图(DAG)来表示仓库历史。 -
• 元数据,包括作者、时间戳和提交信息。
Git 使用增量压缩高效地存储提交,这意味着它只记录变化,而不是存储文件的完整副本。
提交 — 树 — blob 的关系图(图片来源于 这里)
已删除文件及其可恢复的原因当使用 git rm 删除文件,或简单地将文件从工作目录中移除并提交时,文件会从最新的快照中消失,但仍然存在于仓库的历史记录中。原因如下:- Git 提交历史是不可变的:一旦创建提交,其数据就被存储在 .git/objects 中,即使之后没有任何分支或标签引用它,这些数据也会继续存在。未引用(悬挂)的对象不会立即被移除 —— 通常会保留大约两周,之后才可能被垃圾回收机制清理。- 引用(Refs)使对象保持存活:Git 会在 heads、tags 和 remotes 目录中维护引用,因此即使在后续提交中删除了文件,较早的提交(=trees)仍然包含该文件。
要彻底从历史中删除一个文件,必须使用 git filter-branch、git-filter-repo 等工具重写历史,或者手动变基并运行垃圾回收(带 prune 参数)以清除不可达对象 —— 祝你好运。不过,如果仓库是公开的,文件可能早已被其他人复制或克隆,因此发现并吊销泄露的 API 密钥、令牌、秘密和会话至关重要。
为了更好地理解这些概念,我搭建了一个小平台,用来查看和分析 Git 仓库中的文件目录变更,以可视化展示哪些对象被创建、修改或删除。显然,这对于这个项目来说有些大材小用,但当时 coding 氛围正浓,花了不到 5 分钟就搞定了,所以为什么不做呢
精华部分 —— 那么我们如何获取所有已删除的文件?:
-
• 通过比较父子提交的差异恢复已删除的文件。 -
• 使用 *git unpack-objects < .git/objects/pack/pack-.pack* 解包所有 .pack 文件。 -
• 使用 *git fsck — full — unreachable — dangling* 查找悬挂对象。
为了收集所有已删除的文件,我遍历了所有提交记录,并对每一个提交与其父提交进行比较(git diff)。如果存在差异且有文件被删除(标记为 D),我就通过 (git show) 恢复这些文件并将它们保存到磁盘上。显然,这不是最好的方式,也不是最高效的方法,但足够用了。以下是一个简单的 PoC 脚本,正是用来完成这一工作的:
#!/bin/bash# cd to cloned repomkdir -p "__ANALYSIS/del"# Extract all commits and process each commitgit rev-list --all | while read -r commit; do echo "Processing commit: $commit" # Get the parent commit parent_commit=$(git log --pretty=format:"%P" -n 1 "$commit") if [ -z "$parent_commit" ]; then continue fi parent_commit=$(echo "$parent_commit" | awk '{print $1}') # Get the diff for the commit git diff --name-status "$parent_commit" "$commit" | while read -r file_status file; do # Replace / with _ for filenames in binary_files_dir safe_file_name=$(echo "$file" | sed 's///_/g') # Handle deleted files if [ "$file_status" = "D" ]; then # Handle binary files echo "Binary file deleted: $file" | tee -a "__ANALYSIS/del.log" echo "Saving to __ANALYSIS/del/${commit}___${safe_file_name}" git show "$parent_commit:$file" > "__ANALYSIS/del/${safe_file_name}" fi donedone
收集目标
既然我已经有了一个可以恢复已删除文件的 PoC,下一步就是尽可能多地收集相关的 GitHub 仓库。那么,什么算是相关的呢?当然是任何拥有活跃漏洞赏金计划(Bug Bounty Program)的公司。我整理了一个公司名称列表,涵盖了所有公开已知的漏洞赏金项目,以及这些年我受邀参与的一些私有项目。对于公开的漏洞赏金项目,我主要参考了以下来源:
-
• https://hackerone.com/bug-bounty-programs- https://www.bugcrowd.com/bug-bounty-list/- https://github.com/Lissy93/bug-bounties- https://github.com/trickest/inventory- https://github.com/disclose/bug-bounty-platforms- https://github.com/projectdiscovery/public-bugbounty-programs
此外,我还觉得收集所有至少有一个 GitHub 仓库获得超过 5000 颗星的账户(主要是组织)可能也会很有趣。最后我用一行命令搞定了:
for page in {1..100}; do gh api "search/repositories?q=stars:>5000&sort=stars&order=desc&per_page=50&page=$page" --jq '.items[].full_name'; done | cut -d '/' -f 1
组织账户
我列出了所有公司的名称,并将其保存为 companies.txt 文件。接下来,我需要找到它们的公开 GitHub 账户。我考虑了不同的方式来实现这个目标,最终选择了最懒且最傻的方法 —— “AI”。我将公司名称批量发送到 grok、perplexity 和 OpenAI,并友好地请求它们“在线搜索并找到与以下组织相关联的 GitHub 账户”。这不是完美的,我不得不修正一些它们编造的账户,但总体来说,这个方法还算有效。
I使用 AI 将公司/组织名称转换为其 GitHub 账户
我在早期做出的一个重要观察是,许多公司会维护多个 GitHub 账户。例如,他们可能会为主要的工程团队、研究、QA/测试、社区等设置不同的账户。每个账户中都可能包含有价值的信息,且来自不同部门的开发人员可能会不小心在这些账户中泄露敏感数据。因此,我确保特别寻找那些名称包含“lab”、“research”、“test”、“qa”、“samples”、“hq”和“community”等的账户。
以下是我使用的具体提示:
I have a task for you. I will provide a list of companies, and your job will be to search the internet for all GitHub accounts associated with those companies. This includes official, affiliated, open-source, and community-related repositories. Please provide only the GitHub URLs in a clean, bullet-point list. Is that clear?
此外,我还审查了我收集到的所有 GitHub 账户和仓库,以识别哪些是其他项目的分支。对于每个分支的仓库,我追溯到原始源,并将相关的 GitHub 账户添加到我的监控列表中。我的逻辑是,如果 Vendor-A 创建了一个项目,并且该项目被其他公司(在我的列表中)分叉,那么 Vendor-A 可能还有其他仓库 —— 这些仓库可能包含泄露的秘密 —— 可能会影响那些分叉了他们其他项目的公司。
我在分析所有仓库及其连接时使用的其中一个图表
总的来说,我有成千上万的 GitHub 组织账户,现在是时候构建自动化了。
构建自动化
这个自动化非常简单,它需要克隆所有组织的所有项目,恢复所有已删除的文件,并搜索仍然有效的密钥。以下是伪代码:
- foreach company in companies: - foreach repo in comapny.repos: - restore all deleted files - foreach file in files: - collect secrets - foreach secret in secrets: - is secret active? - notify via Telegram bot
总体来说,我需要实现六个步骤,包括准备机器、克隆所有仓库、恢复所有已删除的文件、搜索敏感信息、对验证的敏感信息进行通知、删除仓库并继续。以下是每个步骤如何协同工作的详细介绍。
设计自动化
1 — 准备机器
在这个项目的整个处理过程中,我使用了10台服务器,其中一些是私有云计算(如 EC2),一些是 VPS,一些是物理计算机和树莓派。我确保每台计算机至少有120GB的空闲硬盘空间。然后,我将 GitHub 组织列表分成了10个部分,并将每个部分加载到不同的服务器上。
2 — 克隆所有仓库
为了获取组织的整个 GitHub 仓库列表,我使用了 gh 命令行工具,代码如下:
for REPO_NAME in $(gh repo list $ORG_NAME -L 1000 --json name --jq '.[].name');do FULL_REPO_URL="https://github.com/$ORG_NAME/$REPO_NAME.git" git clone "$FULL_REPO_URL" "$REPO_NAME"done;
3 — 恢复所有已删除的文件
对于每个克隆的项目,我使用三种技术恢复所有已删除的文件:
-
• 通过对比父子提交恢复已删除的文件(参见上面的 Bash 脚本)。 -
• 使用 *git unpack-objects < .git/objects/pack/pack-.pack* 解包所有 .pack 文件。 -
• 使用 *git fsck — full — unreachable — dangling* 查找悬挂对象。
4 — 搜索活跃的敏感信息
一旦所有文件恢复完成,我运行了 TruffleHog,它是一个非常优秀的敏感信息扫描工具,能够深入代码仓库并找到敏感信息、密码和密钥。它支持检测和验证超过800种不同的密钥类型,并且整体运行速度非常快。它还可以检测 base64 编码的密钥以及一些压缩数据格式,这对我们来说非常有用。
我使用了 Trufflehog 并加上 — only-verified 标志,仅搜索 有效且已验证 的敏感信息。我还使用了 filesystem 参数,强制它在磁盘上的文件中进行搜索——这也包括恢复的已删除文件。
trufflehog filesystem --only-verified --print-avg-detector-time --include-detectors="all" ./ > secrets.txt
运行 Trufflehog 在本地克隆仓库的另一个好处是,它还扫描了 .git 目录,并且由于它可以解压并扫描 zlib 压缩流,大部分松散对象都能“免费”被扫描。它还扫描了关闭的 .pack 文件,这有时也给我带来了令人惊讶的好结果。
一个问题出现了——如果 Trufflehog 能解压并扫描 git 对象,为什么还要费劲恢复已删除的文件呢?因为这样做显著提高了找到敏感信息的成功率。有时候压缩流和 .pack 文件太大,无法处理;有时候它们被多次压缩和包装,工具在扫描原始格式时无法得出结果。通过恢复尽可能多的文件,我在找到泄露的敏感信息时达到了更高的成功率。
5 — 通知已验证的敏感信息
一旦 Trufflehog 找到泄露的敏感信息,我就通过 Telegram 通知我:
curl -F chat_id="XXXXXXXXXXXXX" -F document=@"$ORG_NAME.$REPO_NAME.secrets.txt" -F caption="New secerts - $ORG_NAME - $REPO_NAME" 'https://api.telegram.org/botXXXXXXXXXXXXX:XXXXXXXXXXXXX/sendDocument'
为什么选择 Telegram?我也不清楚,反正这是我用来自动化通知的工具。虽然建立一个小型后台并配上小型数据库会更好,但谁在乎呢
6 — 删除仓库并继续 最后,自动化会删除仓库以节省空间,并继续处理下一个仓库。
发现与奖励
那么我发现了什么?数百个活跃的敏感信息泄露——除了真实的生产令牌,我还遇到了测试账户,甚至还有一些公司精心埋设的诱饵账户,用来在有人尝试使用时被通知。让我们来看看我发现的所有内容:
最有趣的敏感信息
以下是我找到的最有趣的敏感信息和令牌,以及我为它们收到的奖励范围。奖励金额根据多个因素变化,例如影响、令牌的作用范围以及受影响公司在漏洞悬赏计划中的政策和支付表。
-
• GCP 项目/AWS 生产令牌 — 5k–15k 🔥🔥🔥 -
• Slack 令牌 — 3k-10k 🔥🔥 -
• Github 令牌 — 5k-10k 🔥🔥 -
• OpenAPI 令牌 — 500–2000 🔥 -
• HuggingFace 令牌 — 500–2000 🔥 -
• Algolia 管理员令牌 — 300–1000 🔥 -
• Email SMTP 凭证 — 500–1000 🔥 -
• 平台特定的开发者令牌和会话 — 500–2000 🔥
使用泄露的敏感信息,我能够访问超敏感数据
不感兴趣的敏感信息
虽然我确实找到了许多有趣的令牌,但其中一些活跃的令牌与测试账户或甚至诱饵账户相关。事实证明,有一个非常不错的网站叫做 CanaryTokens,它允许你生成诱饵令牌,并在有人尝试使用它们时通知你。这个主意非常棒。
一些其他不感兴趣的重复令牌是一次性账户、用于测试的虚拟用户,以及完全没有权限的 API 令牌,这些通常被故意用于前端。每当遇到这些令牌时,我会直接忽略它们,因为它们根本没有任何影响。
一次性账户和测试私钥并不有趣
让我们看几个例子:
-
• Github虚拟用户私钥:许多项目使用涉及虚拟Github用户的测试私钥。我遇到过几个这样的私钥,它们在数百个仓库中重复出现,例如 aaron1234567890123。 -
• 一次性账户:用于只读或绕过速率限制的账户。 -
• Web3 API开放令牌:我看到成百上千个Web3项目使用开放的API密钥来查询不同的区块链交易或加密货币价格。最流行的API令牌有:Infura 和 Alchemy。 -
• 前端API密钥:一些服务,如 Algolia,要求你在前端使用特定的API令牌。通常这个令牌与只读权限相关,不能修改或影响应用程序。问题出现在开发者不小心使用了权限更大的令牌。例如,使用了Algolia管理员令牌而不是Algolia搜索令牌。 -
• 诱饵令牌:一些公司使用诱饵令牌来在被使用时收到通知。有一些服务可以帮助实现这一点,例如 CanaryTokens。
为什么这些敏感信息会泄露呢?
在分析了多个实际影响的案例后,我将这个问题总结为三种解释——缺乏对Git工作原理的了解、未完全意识到提交了什么(由于二进制文件或隐藏文件),以及盲目相信Git的历史重写工具。
泄露敏感信息的主要原因是二进制文件的删除
-
• 缺乏对Git工作原理的了解:有时开发者不了解Git是如何存储信息的。例如,开发者提交了明文的凭证/令牌/密钥。后来他们意识到犯了错,简单地删除了密钥或文件,但没有撤销密钥。然而,Git会记住所有内容,因此通过恢复所有已删除的文件,任何人都可以找到这些密钥。 -
• 未意识到文件内容:我看到过多个案例,开发者没有使用 .gitignore,并且不小心提交了包含密钥的二进制文件,如 .pyc
(Python编译)文件。后来他们删除了这些文件,却没有意识到这些文件包含了混合着敏感密钥的二进制数据。在其他情况下,我看到有隐藏文件(如 Linux 中以点号开头的文件名,例如 .env)被提交,或者被压缩成档案文件后提交。即使这些文件后来被删除,信息仍然可以恢复,因此密钥仍然会泄露。 -
• 信任Git历史重写工具:我还注意到有一个案例,开发者不小心把密钥提交到了仓库。后来他们意识到错误,删除了这些密钥并尝试使用Git历史重写工具使其消失。然而,.pack文件中仍然有一个引用,我成功恢复了这些密钥。幸运的是,我负责任地报告了这一点,他们能够修复问题,没有造成任何损害。
大多数泄露的密钥都存在于已提交到仓库并后来删除的二进制文件中。这些文件通常是由编译器或自动化过程生成的。一个常见的例子是 .pyc
文件,这是当一些Python解释器编译源代码时生成的Python字节码文件。这些文件通常是无意中提交的。其他例子包括编译器生成的调试文件,如 .pdb
文件,这些文件有时也会被错误地提交。
总结
我认为,将大量GitHub仓库克隆到本地,并提取/恢复已删除的文件,证明是一种有效的扫描和发现泄露密钥的策略。此外,我对Git的内部机制有了更深入的理解,并在此过程中构建了几个自定义工具,其中一个工具 HexShare 是公开的。在这个过程中,我还向多家500强公司报告了泄露的密钥,并获得了大约64,000$的奖励。
声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。
原文始发于微信公众号(白帽子左一):漏洞赏金故事 | 通过已删除的文件赚到 6.4 万$
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论