删掉的密钥,还留在历史里
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 个仓库。
同一个仓库,由 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 会列出
每一个令牌出现在错误主机上的请求。
密钥泄露后的善后
密钥泄露后的标准建议是:轮换它、用 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 服务商的模式。下一波会加入更多模式,而它 读取的历史只会越来越长。
让你的代理在它们接触的密钥都是替身的地方运行,它们留下的历史就不会有任何买家 能用的东西。想进一步了解替身如何运作、泄露时会发生什么,请阅读 那个泄露了自己密钥的代理 和 那把密钥从未过期。