tl;dr
GitHub Archive 会记录每一次公开提交,即使是开发者试图删除的提交也不例外。强制推送通常会通过重写 Git 历史记录来掩盖诸如凭据泄露之类的错误。据我们所知,GitHub 会永久保留这些悬空提交。在归档中,它们显示为“零提交”
PushEvents。我扫描了自 2020 年以来所有零提交强制推送事件,并发现了价值2.5 万美元的漏洞赏金秘密。
我们与 Truffle Security 合作,开源了一款新工具,用于扫描您自己的 GitHub 组织中的这些隐藏提交(点击此处试用)。
新的开源工具Force Push Scanner可以识别悬空提交中的秘密信息。
本文由白帽黑客 Sharon Brizinov 撰写,是Truffle Security 研究征稿项目的一部分。我们最初是在 Sharon 广为流传的文章《我如何从已删除文件中赚取 64k 美元》之后与他取得联系的。在这篇文章中,他使用 TruffleHog 挖掘了 GitHub 公共仓库中的高价值秘密。在这篇后续文章中,Sharon 将研究范围扩大到访问 GitHub 上 100% 的已删除提交。他深入探讨了我们最感兴趣的领域之一:隐藏在已删除 GitHub 提交中的秘密。
概述
-
删除提交意味着什么?
-
GitHub 事件 API
-
查找所有已删除的提交
-
构建自动化
-
探寻具有影响力的秘密
-
案例研究——防止大规模供应链中断
-
概括
背景
我叫Sharon Brizinov,虽然我通常专注于 OT/IoT 设备中的底层漏洞和利用研究,但我偶尔也会涉足漏洞赏金猎取。
我最近发表了一篇博文,探讨如何挖掘GitHub代码库中悬空数据块(dangling blob)里隐藏的秘密,引发了热烈的讨论。博文发表后,我与包括Truffle Security首席执行官Dylan在内的许多人进行了交流,他为我提供了许多有趣的思路,让我能够继续探索大规模秘密挖掘的新方法。我决定绘制一张思维导图,梳理所有与此相关的知识,并尝试提出一些新的想法。
我就不放我那乱七八糟的草图了,这里汇总一下我重点关注的项目、博客、想法和资源(强烈推荐):
最终,我想到了一个简单的办法——利用 GitHub Event API 和GitHub Archive项目,扫描所有零提交推送事件(已删除的提交),从中查找秘密信息。所有信息都已掌握,我只是将它们整合起来,构建了一个大规模的自动化流程来搜寻秘密信息。
在这篇博客中,我将描述我从理解为什么你永远无法真正删除 GitHub 中的提交,到如何找到所有提交并围绕它构建自动化的过程。
删除提交意味着什么?
在我之前的博文中,我讨论了如何发现 GitHub 仓库中那些看似已被删除的文件。具体来说,我能够重建悬空 blob——那些已被删除且不再被任何提交或树引用的对象……至少我是这么认为的。在与 Truffle 团队交流后,我发现这些孤立的 blob 实际上还对应着一些孤立的提交。经过一番研究,我成功地在 GitHub 上大规模地找到了所有这类孤立的提交。
假设你不小心提交并推送了一个秘密到你的代码仓库。下一步该怎么办?通常,你需要将 HEAD 重置到上一个提交,然后强制推送更改,这样就能有效地删除当前提交,使其不再被引用——本质上就是删除它。以下是具体操作方法:
但正如neodyme和TruffleHog所发现的,即使提交已从仓库中删除,GitHub 也永远不会忘记。如果你知道完整的提交哈希值,就可以访问到看似已删除的内容。此外,你甚至不需要完整的提交哈希值,正如 TruffleHog 所发现的——只需暴力破解前四位十六进制数字就足够了。
推力教程
让我们用我自己的仓库test-oops-commit来看一下实际效果。尝试找到已删除的提交 – 9eedfa00983b7269a75d76ec5e008565c2eff2ef。
为了帮助我们更直观地了解提交情况,我编写了一个简单的 bash 脚本,用于显示提交树 blob对象:get_commits.sh
我们首先创建一个简单的仓库,其中包含一个提交(一个README.md文件):
接下来,我们创建一个名为 `secret` 的新文件,secret.txt其中包含我们的密钥my-password-is-iloveu。我们不小心提交并将密钥推送到了 GitHub。
我们查看提交树,发现有一个新的提交9eedfa……它与一个新的提交树和一个新的文件 blob 相关联。当我们运行`git commit -l` 或通过 GitHub 网页访问该文件时,secret.txt也会看到相同的情况。git rev-list -- allgit log
糟糕!我们发现了错误,于是通过将分支的 HEAD 移动到上一个提交并强制推送来删除该提交:
让我们删除本地仓库,重新克隆仓库,然后检查提交树。呼,果然不出所料,提交确实被删除了!
但我们记住了提交哈希值,我们在 GitHub 上在线检查,发现该提交仍然可以访问——9eedfa00983b7269a75d76ec5e008565c2eff (即使使用四个十六进制数字9eef也可以访问)。然而,这次我们收到一条消息,提示该提交已被删除或不属于此仓库中的任何分支。
为什么会发生这种情况?
当你在重置后强制推送(也就是git reset --hard HEAD~1执行`git push` git push --force)时,你会从你的分支中移除 Git 对该提交的引用,从而使该提交无法通过正常的 Git 导航(例如 `git config` git log)访问。然而,该提交仍然可以在 GitHub 上访问,因为 GitHub 会存储这些引用日志。
为什么?我也不确定,但 GitHub 的确给出了一些线索。在我看来,GitHub 远比一个简单的 Git 服务器复杂得多。它包含很多层,例如 pull request、fork、私有/公共设置等等。
我的猜测是,为了支持所有这些功能,GitHub 会存储所有提交记录,并且从不删除它们。以下是一些需要考虑的情况:
-
什么是拉取请求?正如Aqua Security所写,它们只是临时分支,可以通过使用以下命令获取所有引用来获取:
-
git -c "remote.origin.fetch=+refs/*:refs/remotes/origin/*" fetch origin
-
-
GitHub 的 fork 网络是如何运作的?当你“fork”一个仓库时会发生什么?所有数据都会被复制,包括你可能删除的提交。
对于这些情况,以及可能还有许多其他情况(审计?监控?),Github 会存储所有提交,即使你强制推送最新代码并“删除”提交,也不会删除它们。
GitHub 事件 API
好的,所以提交实际上并没有被删除。没问题。但是你仍然需要知道完整的提交哈希值,或者至少是前四位十六进制数字(忽略冲突16^4=65536)。TruffleHog 正好有一个工具可以做到这一点,但正如你所想,它处理所有这些提交非常慢。它无法很好地扩展,处理单个仓库可能需要一两天的时间。
但还有另一种方法,一种更快捷的方法,我现在很乐意与您分享。GitHub Event API 是 GitHub REST API 的一部分,它允许用户检索有关 GitHub 中发生的事件的信息。事件代表 GitHub 中的各种活动,例如:
-
推送代码
-
打开或关闭问题或拉取请求
-
创建存储库
-
添加评论
-
派生一个仓库
-
主演一个仓库
试试看:
几点说明:
-
无需API令牌或身份验证!
-
您可以在这里查看GitHub支持的所有事件。
-
事件以接近实时的方式记录,但可能会有几秒钟的延迟。
-
仅适用于公共代码库。
所以,我们可以监控所有 GitHub 公共仓库的提交数据,并存储所有哈希值。这样就不用再猜测提交哈希值了!没错,但这工作量太大了。我们说的是每小时数百万条事件,那么过去的事件怎么办?它们会丢失吗?
幸运的是,多年前一位名叫Ilya Grigorik的优秀开发者决定启动一个项目,该项目监听 GitHub 的事件流并对其进行系统归档。该项目是开源的,名为GH Archive,其网站为gharchive.org。因此,例如,如果我们想要获取 1 月 1 日下午 3 点(UTC 时间)前后 GitHub 的所有公开活动,只需从这里下载即可:https://data.gharchive.org/2015-01-01-15.json.gz。
以下是PushEvent该2015-01-01-15档案库中的一个随机样本:
查找强制推送已删除的提交
为了仅识别强制推送事件中被删除的提交,我们可以查找包含零个提交的推送事件。为什么 Git 推送事件会没有提交呢?这表明强制推送重置了分支——本质上只是移动了 HEAD 指针,而没有添加任何新的提交!我称之为“ Oops commit”或“推送事件零提交”。
我们来看一个简单的例子。我们将下载一个随机存档,并搜索此类事件。
如果我们随机选择一种目标事件类型,会发现commits数组为空(提交数为零)。如果我们查看before被“删除”的提交(移动到 HEAD^1 之前的 HEAD,也就是“之后”的 HEAD),会发现 GitHub 竟然在 10 年后仍然保留着它的记录!
链接在此 – https://github.com/grapefruit623/gcloud-python/commit/e9c3d31212847723aec86ef96aba0a77f9387493
而且,被删除的不一定只是单个before提交。有时强制推送会一次性覆盖多个提交。
给定一个 GitHub 组织(或用户)、仓库名称和提交哈希值,使用 Git 访问权限很容易扫描“已删除”提交的内容以查找秘密信息:
这段脚本:
-
以最小的方式克隆仓库。
-
--filter=blob:none:省略文件内容(blobs),仅保留历史记录/树/提交。 -
--no-checkout:未检出工作目录(尚未显示任何文件)。
-
-
获取特定的提交(
<before>)。 -
使用 TruffleHog 扫描秘密。
-
TruffleHog 会自动下载需要扫描的文件内容(blobs)。
-
before此命令将从目标提交开始,向前追溯到该分支的起始提交,搜索所有提交中的秘密信息。这确保了强制推送覆盖多个提交所导致的所有数据都会被扫描;但是,它也会扫描一些非悬空提交。我们发布的开源工具效率更高,它只扫描实际的悬空(已取消引用)提交。
-
GitHub 没有为 Git 操作指定确切的速率限制,但过度克隆或获取存储库可能会触发延迟或速率限制(参见此处)。
此外,我们还可以使用其他方法,通过 GitHub API 或直接通过 Github Web UI 查询特定的已删除/悬空提交。
GitHub API
使用 GitHub 的 REST API 查询提交补丁:
https://api.github.com/repos/<ORG>/<REPO-NAME>/commits/<HASH>
注意:注册用户每小时的查询次数严格限制为 5,000 次,非注册用户则仅为 60 次。服务器响应头会显示用户剩余的 API 调用次数。x-ratelimit-remaining
通过Github.com直接访问网页
您也可以直接从GitHub.com获取提交详情。
以下是三种通过 GitHub 网站访问任何提交记录的不同示例:
https://github.com/<组织>/<仓库名称>/commit/<哈希值>
https://github.com/<ORG>/<REPO-NAME>/commit/<HASH>.patch
https://github.com/<ORG>/<REPO-NAME>/commit/<HASH>.diff
虽然没有明确的速率限制,但在高使用量下无法保证访问,并且其 WAF 可能随时阻止请求,恕不另行通知。
构建自动化
我们已经具备了所有条件——我们可以获取所有 GitHub 事件数据,搜索所有PushEvent Zero-Commit事件,获取“已删除”的提交(before哈希值),然后使用 TruffleHog 扫描活跃的密钥。让我们开始吧。
你知道吗?无需自己开发,因为我们与 Truffle Security 的研究团队合作,开源了一款新工具,用于搜索整个 GitHub 归档中由您的 GitHub 组织或用户帐户提交的“Oops Commits”。由于整个 GitHub 归档已作为Google BigQuery 公共数据集提供,该工具会扫描 GHArchive PushEvent 数据中的零提交事件,获取相应的提交,并使用 TruffleHog 扫描其中的秘密信息。
免责声明:我们发布此工具旨在帮助蓝队成员评估潜在风险。请谨慎使用。
以下是一个入门命令:
为了进行这项研究,我使用我们开源工具的定制版本扫描了 GitHub 自 2020 年以来所有Oops 项目的提交记录。哇!里面藏着好多秘密!
探寻具有影响力的秘密
运行自动化程序后,我发现了数千个活跃的秘密。但我该如何找到与最具影响力的组织相关的最有趣的秘密呢?我的成功秘诀是三步走:人工搜索、基于氛围的筛选工具和人工智能。
手动搜索
首先,我手动探索和处理数据——说白了,就是亲力亲为。我开发的自动化程序会将每个新发现的秘密存储在一个结构良好的 JSON 文件中。以下是其中一个文件的示例:
在这个阶段,我手动检查了文件,寻找有价值的信息。例如,我过滤掉了所有使用通用邮箱地址(例如gmail.com、outlook.com、mail.ru等)的作者提交的代码,并重点关注使用公司邮箱的作者提交的代码。虽然这并非完美无缺,但却是一个良好的开端,我从中发现了一些非常重要的密钥。
为了解特定令牌的影响,我尝试使用开源工具(例如secrets-ninja)和一些自定义脚本来确定密钥的所有者及其访问权限。在研究过程中,我了解到 Truffle 安全团队发布了一个开源工具TruffleHog Analyze来执行此操作。它已内置于 TruffleHog 中;您只需运行即可。 trufflehog analyze
注意:我只在特定漏洞赏金或漏洞披露计划的范围内才进行这种额外的秘密枚举。
一旦我发现相关或有趣的东西,我就会通过漏洞赏金计划或直接通过电子邮件进行报告。
秘密分诊的氛围编码
经过几百次手动检查后,我受够了,决定扩大我的密钥审查规模。我使用 Vercel v0,以 Vibe 代码的方式搭建了一个完整的平台,用于对这些“Oops Commit”密钥进行分类。
这个平台非常简单。它只有前端界面(完全没有后端),接收一个包含扫描器生成的 JSON 文件的 .zip 文件。然后,它会将这些文件以非常易用的表格形式呈现,方便我快速查看并标记已查看的内容。这种方法非常高效,我结合使用各种筛选条件,很快就找到了许多隐藏的宝藏!
我还添加了一些图表和饼图,何乐而不为呢?看着这些图表,我立刻发现了一些有趣的见解。
首先,如果您查看下面的时间序列图,就会发现年份与活跃密钥数量之间存在明显的直接相关性——很可能是因为较旧的密钥已被撤销或过期——理应如此!
其次,MongoDB 密钥泄露最多。根据我对数据的分析,这是因为很多初级开发者和计算机科学专业的学生泄露的大多是无关紧要的业余项目 MongoDB 凭证。泄露的最有价值的密钥是 GitHub PAT 令牌和 AWS 凭证。这些密钥也带来了最高的赏金!
最后,我绘制了泄露有效凭据的文件的频率图,结果很明显——你的.env 文件需要额外的保护!
除了 .env 之外,最容易泄露文件的文件名还有:.env, index.js, application.properties, app.js, server.js, .env.example, docker-compose.yml, Unknown, README.md, main.py, appsettings.json, db.js, .env.local, settings.py, config.py, app.py, config.env, application.yml, config.json, , , , , , , , , , , , , , , , , , , , , , ,config.jsWeatherManager.swift.env.productiondatabase.jshardhat.config.jsscript.js App.js.env.developmenthardhat.config.tsindex.tsconfig.tssecrets.txtmain.jsindex.htmldocusaurus.config.jsdefault.jsonDockerfilevercel.jsonapplication-dev.ymlapi-client.tsdocker-compose.yamlapi_keys.py
一切皆人工智能
我对基于 Vibe 编码的密钥审查平台相当满意。然而,密钥审查目前仍需手动操作。理想情况下,该流程应自动解析所有密钥,并尽可能提取关联账户的基本信息。这些数据随后可传递给基于 LLAMA 的代理,由其分析并识别潜在的有价值密钥。本质上,目标是构建一个离线代理,能够从漏洞赏金或影响驱动的角度判断哪些密钥具有重要意义。
在我的朋友Moti Harmats的帮助下,我开始着手开发这个项目,但还有很多工作要做,所以目前我不会发布。不过,这里先给大家预览一下我目前开始制作的内容:
案例研究:防止大规模供应链中断
我在一次已删除的提交中发现的秘密之一是属于一位开发者的GitHub 个人访问令牌(PAT)。这位开发者在提交隐藏的配置文件(点文件)时意外泄露了这个秘密。我分析了这个令牌,发现它拥有对所有Istio代码库的管理员权限。
Istio 是一个开源服务网格,它提供了一种透明且与编程语言无关的方式,可以灵活便捷地自动化应用程序的网络功能。它旨在管理分布式应用程序中微服务之间的通信,提供流量管理、安全性和可观测性等功能,而无需修改应用程序代码。
Istio主项目拥有3.6 万颗星和8000 个分支。Istio 被众多运行复杂分布式应用程序的组织和团队所使用,尤其适用于采用微服务架构的团队。这其中包括谷歌、IBM、红帽等众多大型企业。
我拥有所有 Itsio 代码库(数量众多)的管理员权限。我可以读取环境变量、修改流水线、推送代码、创建新版本,甚至删除整个项目。这里存在大规模供应链攻击的风险,令人担忧。
幸运的是,Istio 有一个维护良好的报告页面,团队在问题报告后立即采取行动,撤销了 GitHub PAT 权限。谢谢!
概括
这是一个非常有趣的项目。我整合了一些已知的发现,并创建了一个可靠的自动化程序,它可以扫描并找到数千个活跃的秘密,甚至包括一些埋藏多年的秘密。我还有机会用代码编写了一个秘密搜寻平台,它拥有一些很棒的功能,让我能够大海捞针般地找到有价值的信息,并在此过程中获得了大约 2.5 万美元的赏金和深深的感谢。
删除提交是安全的这种普遍假设必须改变——一旦秘密信息被提交,就应视为已泄露,必须尽快撤销。这适用于 Git 代码块、Git 提交以及任何其他上线的内容。