← 返回所有文章
发布于 · 作者 Renaud Deraison

删掉的密钥,还留在历史里

10 月 8 日,一场名为 GhostAction 的攻击活动利用两个被劫持的维护者账号,把一个叫 security-audit.yml 的 workflow 提交进 346 个仓库,其中包括 Uber 的 athenadriver,以及拥有 1.8 万星的 pyxel。到 10 月 9 日,Socket 统计的账号已超过 500 个,仓库达数万个。这个 workflow 会运行 git log -p --all,在有史以来的每一个 commit 里 grep Anthropic、OpenAI、OpenRouter、GitHub 和 AWS 的密钥,连下一个 commit 就删掉的也不放过。在 Bromure Agentic Coding 里,代理手上永远只有替身,所以它写进仓库历史的密钥,对找到的人毫无价值。

从文件里删掉一把密钥,只需要一个 commit。前一个 commit 会一直保留这把 密钥,直到仓库不复存在。

10 月 8 日星期四 13:40 UTC,游戏引擎 pyxel 作者 Takashi Kitao 的 GitHub 账号开始提交一个名为 .github/workflows/security-audit.yml 的文件。四分钟后, 它已出现在他的 27 个仓库里。21:10 UTC,Uber athenadriver 的原作者账号 henrywoo 开始了同样的扫荡。到 21:26,它已覆盖 318 个仓库,其中有 279 个 fork,以及该账号拥有写权限的 uber/athenadriver 本身。

Socket 于 10 月 9 日发布了 分析,把这些 commit 归于 GhostAction,一个在 2025 年 9 月首次出现的凭据窃取活动。当天结束前, Socket 的更新已统计出超过 500 个 GitHub 账号,自 10 月 7 日起把这个 workflow 提交进“数万个仓库”。The Hacker News 和 StepSecurity 也报道了同一波攻击。Socket 发现,这个 workflow 已在受影响的 仓库里运行过,而且在文章发布时,仍留在 kitao/pyxel 和 uber/athenadriver 的默认分支上。

security-audit.yml 做了什么

这个 workflow 叫“Security Audit”。它在 workflow_dispatch 时运行,也在任何 分支或 tag 的每一次 push 时运行,没有任何过滤。它的 checkout 步骤使用 fetch-depth: 0,会拉取所有分支、所有 tag 和完整的历史。接着,单独一个步骤 完成两件事。

第一件事冲着仓库的 GitHub Actions 机密而来。攻击者在提交这个 workflow 之前, 先读了仓库现有的 workflow,把其中用到的机密名称写进 payload。以 pyxel 为例, 就是 CARGO_REGISTRY_TOKEN、PERSONAL_ACCESS_TOKEN、PYPI_PASSWORD 和 PYPI_USERNAME。

第二件事是这一波新加的。这个步骤先用 13 种凭据模式 grep 工作区,再运行 git log -p --all,用同样的模式 grep 最多 20 万行历史:

  • AI 服务: Anthropic(sk-ant-)、OpenAI 项目密钥(sk-proj-)、 OpenRouter(sk-or-)
  • 源代码管理: GitHub 经典令牌(ghp_)、细粒度令牌(github_pat_)、 GitLab 令牌(glpat-)
  • AWS: 访问密钥 ID、临时密钥 ID、私有密钥和会话令牌。这个步骤还会保留 每个密钥 ID 前后两行内容,所以提交在旁边的私有密钥也会被一并带走。
  • SaaS 与云: Google API 密钥、Slack 令牌、SendGrid 密钥

所有东西都通过一次 curl POST,以未加密的 HTTP 发往 193.32.204.199。每次 运行都会上报仓库名称,不管有没有找到东西。

Socket 点出了目标人群。henrywoo 的仓库中有两个是 pyllama 和 chatllama,都是机器学习项目,其贡献者和 fork 者“很可能持有这类密钥”。 Anthropic 或 OpenAI 的密钥可以当作计费的推理服务转卖,而通过它发出的每一个 prompt,都会经过买家之手。

历史会留下一切

Socket 的修复建议里有一句话,任何曾经靠编辑文件来清理泄露密钥的人都该担心: “轮换在任何 commit 中找到的每一个凭据,无论它是否还在当前的树里。”在下一个 commit 删除的密钥,仍留在前一个 commit 里。git log -p --all 两个都能读到。

编程代理让这段历史增长得很快。GitGuardian 的 State of Secrets Sprawl 2026 发现, 由 Claude Code 共同撰写的公开 commit 泄露机密的比例为 3.2%,而整个公开 GitHub 的基线是 1.5%。同一份报告统计,2025 年与 AI 服务相关的泄露机密有 1,275,105 条,比前一年多了 81%。

看过代理让一个失败的集成测试通过的人,对这个模式都不陌生。代理的环境里有 密钥。它把密钥写进配置文件、测试 fixture,或还没被 ignore 的 .env,因为 这样测试就能通过。它提交。之后,开发者或代理把密钥移到环境变量,再提交一次。 树看起来很干净。历史里有密钥,每个 fork 都有这段历史,而一个 fetch-depth: 0 的 workflow 会读取它。

写下 workflow 的那个令牌

Socket 没有查明这两个账号是怎么被夺走的。The Hacker News 点出了最可能的 来源:来自信息窃取程序日志或凭据转储的 personal access token。信息窃取程序的 日志来自开发者的电脑。在笔记本上,GitHub 令牌放在 ~/.config/gh/hosts.yml、 ~/.git-credentials,也常在 GH_TOKEN 里,任何以开发者身份运行的东西都读得 到,包括编程代理安装或运行的任何东西。

一个令牌就够了。有了它,攻击者就能列出该账号可写入的仓库,读取它们的 workflow 找出机密名称,再逐一提交。henrywoo 的扫荡在 16 分钟内覆盖了 318 个仓库。

一个被盗的令牌、一个 workflow、每一个 commit维护者的笔记本~/.config/gh/hosts.yml~/.git-credentialsGH_TOKEN=ghp_… (真品)窃取程序日志→ 令牌待售THN 推测的来源security-audit.yml:16 分钟 318 个on: push (any branch), workflow_dispatchfetch-depth: 0git log -p --all | grep 13 patternsconfig/test.py 的历史a1f3 + KEY="sk-ant-api03-…" ← 找到c92e - KEY="sk-ant-api03-…" + KEY=os.environ["…"]树很干净,密钥仍在历史里。193.32.204.199POST (明文 HTTP)REPO=…PYPI_PASSWORD=…sk-ant-api03-…AKIA_CTX_START…现存的密钥和已删的一样每个 fork 都带着同一段历史。仅 henrywoo 名下,Socket 就数到 279 个 fork。
GhostAction 的攻击链。来自开发者电脑的令牌提交了 workflow,workflow 读取有史以来的每一个 commit,而那段历史里的每一把密钥,无论现存还是已删除,都在一次 POST 中发出。

同一个仓库,由 Bromure 机器写成

在 Bromure Agentic Coding 里,代理在你 Mac 上的 Linux 虚拟机中运行,虚拟机发出 的每个 HTTP 请求都会经过主机上的代理服务器。你的 Anthropic 密钥、GitHub 令牌 和 AWS 密钥都以加密形式留在 Mac 上。现在,把 GhostAction 放到一个由 Bromure 机器写成的仓库上,以及写它的开发者身上。

历史里存的是替身

在虚拟机里,每个凭据都是外形与真品一致的替身:ANTHROPIC_API_KEY 是 sk-ant-api03-brm-…,OpenRouter 密钥是 sk-or-v1-brm-…,GH_TOKEN 和 ~/.git-credentials 存的是 ghp_ 加上 36 个字符。每一个都是用 HKDF-SHA256 从真实值和每次安装各自的 salt 派生而来,无法还原成真实值。代理服务器在线路上 才把真实值换进去,而且只针对发往该凭据所属主机的请求。

所以,当代理为了让测试通过而把密钥写进 config/test.py,它写的是它手上的 东西。它提交时,commit 里是 sk-ant-api03-brm-…。之后清理用的 commit 来了, 历史保留的也是同一个替身。

GhostAction 的 grep 会找到它。sk-ant- 匹配 sk-ant-api03-brm-,ghp_ 匹配 GitHub 令牌的替身。发往 193.32.204.199 的 POST 会把它们带走,买家 会拿去试。Anthropic 拒绝这把密钥,GitHub 拒绝这个令牌,因为唯一能把它们变成 有效凭据的,是你 Mac 上的代理服务器,而且只针对离开你虚拟机的请求。Socket 建议轮换任何 commit 中找到的每一把密钥,对这个仓库来说答案很简短:这些密钥 从来没有在你的 Mac 之外生效过,而 AI 服务商的替身名称里带有 -brm-,扫描 历史时一眼就能认出。

笔记本上的令牌也是替身

这场攻击活动的另一半是账号本身。在 Bromure 机器里运行代理,它们使用的 GitHub 令牌就留在 Mac 上。在虚拟机里运行的任何东西,无论是软件包的安装脚本、 窃取程序还是 prompt 注入,在 ~/.git-credentials 和 GH_TOKEN 里找到的都是 ghp_ 替身。攻击者在自己的电脑上重放它,只会从 GitHub 拿到 401。

窃取程序还得把拿到的东西发到某个地方,而那个请求会经过代理服务器。替身只有 一个合法目的地,就是它被生成时对应的主机。代理服务器会检查每个外发请求的 请求头和正文,找出任何要发往别处的替身。一个正要发往窃取程序服务器的 ghp_ 令牌会拿到 HTTP 451,什么都不会转发,虚拟机暂停,Mac 上会弹出 This environment may have been compromised,显示令牌预览、凭据名称、它所属的 主机,以及它被发往的主机。

workflow 的 push 要等你点击

这一类蠕虫不一定等买家。上周 tensorlake 的 payload 直接从开发者的电脑把文件提交进受害者的仓库。新的 Bromure 机器对每个 git 托管平台都使用 Prompt before write。push 是写操作,所以从虚拟机把 security-audit.yml 提交进仓库时,会停在一个 macOS 对话框 Allow write on "github.com" from workspace "…"?,并显示线路上原样出现的 POST /owner/repo.git/git-receive-pack。henrywoo 的扫荡在 16 分钟内做了 318 次这样的写入。从 Bromure 机器发出,第一次就会等你,而你并没有让代理给 318 个仓库加上安全审计。

每次使用令牌都有记录

Security Timeline 会把每一次凭据替换记录为一行 Credential brokering:哪个令牌、哪个主机、哪台机器、什么时间。当你需要知道你的 GitHub 令牌是否在 10 月 8 日 21:10 到 21:26 之间 push 过任何东西,答案就在你的 Mac 上,在一份代理无法修改的记录里。开启追踪时,bromure-cli trace leaks 会列出 每一个令牌出现在错误主机上的请求。

在 Bromure 里:历史里是替身,真密钥从未离开 Mac机器虚拟机 (Ubuntu)GH_TOKEN=ghp_… (替身)ANTHROPIC_API_KEY=sk-ant-api03-brm-…代理的 commit+ KEY="sk-ant-api03-brm-…"窃取程序外发 ghp_ 替身git push security-audit.yml虚拟机里没有真机密可偷代理服务器,在 Mac 上只为凭据自己的主机换入真实值替身发往别的主机 → 451,虚拟机暂停,Mac 上告警git-receive-pack → Prompt before writeSecurity Timeline:每次替换令牌、主机、机器、时间GitHub 历史sk-ant-api03-brm-…GhostAction 的 grep匹配 sk-ant-,将它发出Anthropic: 401GitHub: 401买家拿到的字符串在哪里都无效留在 Mac 上,虚拟机之外Anthropic、OpenAI、OpenRouter 密钥 · GitHub 令牌 · AWS 密钥 · SSH 私钥 · 追踪记录 · Security Timeline
同一条攻击链对上 Bromure 机器。workflow 读到的历史里是替身,窃取程序在虚拟机里找到的令牌是替身,任何把替身带往错误主机的请求都会在 Mac 上被拦下。

密钥泄露后的善后

密钥泄露后的标准建议是:轮换它、用 git filter-repo 改写历史、force-push, 然后祈祷没有任何 fork 或镜像抢先复制了旧的 commit。GhostAction 说明了最后这一步 为什么会失败。Socket 在单个命名空间里就数到 279 个 fork,每一个都带着那段历史, 现在也带着那个 workflow。一把密钥只要进了公开的 commit,就已经回不来了,不管 当前的树看起来如何。

Bromure 机器改变的,是一开始有什么会进到 commit 里。代理对密钥可以随意到 GitGuardian 数据所描述的程度。它可以把密钥粘贴进 fixture、提交一个 .env 然后忘了它。它从来没拿到过一把能用的密钥,所以那段历史里的任何东西都伤不了你。

commit 里是替身

虚拟机里的代理只看得到外形与真密钥一致的替身。被写进文件并提交的密钥, 是一串只有通过你 Mac 上的代理服务器才能用的 -brm- 字符串。

收割到的只有 401

GhostAction 的模式会匹配到替身并把它们发出去。在你的 Mac 之外, Anthropic、OpenAI 和 GitHub 在哪里都会拒绝它们。

被盗的令牌会让机器停下

虚拟机里的 GitHub 令牌也是替身。发往 GitHub 以外的任何地方,就会拿到 451,虚拟机暂停,告警会指出令牌和主机。

push 要等你点击

Prompt before write 会把虚拟机发出的每次 push 拦在显示请求内容的 macOS 对话框,Security Timeline 则记录令牌的每一次使用。

下一次 grep

这是 GhostAction 十三个月来的第三波。第一波在 2025 年 9 月,拿走了 3,325 个 CI 机密。第二波在今年 8 月 31 日到 9 月 30 日,瞄准 772 个仓库中的 2,577 个 机密。这一波加上了完整历史和三种 AI 服务商的模式。下一波会加入更多模式,而它 读取的历史只会越来越长。

让你的代理在它们接触的密钥都是替身的地方运行,它们留下的历史就不会有任何买家 能用的东西。想进一步了解替身如何运作、泄露时会发生什么,请阅读 那个泄露了自己密钥的代理 和 那把密钥从未过期。