被盗的令牌不需要服务器
Wiz Research 追踪了 Gheist:一场自动化攻击活动,它从窃密木马的日志和以往的供应链泄露中获取 GitHub 令牌,再往每个令牌能触及的每个仓库里植入一个导出机密的工作流,目前已有来自 850 多个账号的 13,500 多个分支。机密经由 GitHub 自己的 API 流出,所以没有任何攻击者服务器可以封锁。在 Bromure Agentic Coding 中,窃密木马在开发者机器上找到的 GitHub 令牌是一个替身,从其他任何地方使用都会被 GitHub 拒绝,而机器内发出的每一次写入都要等你在 Mac 上的对话框里确认。
Gheist 没有指挥服务器。运行载荷的是 GitHub,把机密写进日志的是 GitHub,把日志交给任何持有令牌的人的,也是 GitHub。
10 月 25 日,Wiz Research 发布了对 Gheist 的分析,这是它从 9 月 15 日起一直追踪的攻击活动。一个自动化工具拿到被盗的 GitHub 令牌后,列出该令牌能触及的每个仓库,并植入一个会打印仓库机密的工作流。Wiz 已从 850 多个被入侵的账号中找回 13,500 多个被植入的分支;截至 10 月 24 日,这场攻击仍在进行,也仍在变化。The Hacker News 昨天报道了此事。
这个工具只需要一种输入,而 Wiz 说明了它的来源:这些令牌与窃密木马的日志,以及以往供应链入侵中泄露的令牌相符,例如 3 月 Trivy 扫描器被入侵的事件。Wiz 给每位受害者的建议中有这样一条:检查令牌所有者的设备上是否有窃密木马。
Gheist 如何运作
对每个仓库,工具会创建一个名为 ci-sync- 加六个随机字符的分支,并以 ci: sync config 为提交信息提交唯一一个文件 .github/workflows/sync-check.yml。这次提交没有签名,作者记为令牌的所有者。它的 API 请求带着 gheist 这个 user agent,攻击活动也因此得名。
推送会触发工作流。作业会把环境变量和整个 secrets 上下文打印进运行日志,两者都用 base32 编码。Wiz 解释了这招为什么有效:“GitHub 只有在机密值原样出现时,才会在日志中屏蔽它们。”base32 输出与存储的值不一致,所以什么都没有被屏蔽。一个覆盖仓库各部署环境的矩阵,会把环境级别的机密也一并拉出来,而那往往就是生产环境的机密。
接着,工具通过 GitHub API 下载日志,并在几秒后删除那次运行。用 Wiz 的话说:“由于机密是经由 GitHub 自己的 API 流出,所以没有攻击者域名或出站连接可以封锁。”
这个工具一直在改进。有个变种在 9 月 28 日首次运行后的几个小时内,就加上了六分钟的超时、环境矩阵,以及针对每个仓库的载荷调整。10 月 19 日,它又多了一个针对自托管 runner 的作业,搜寻 31 种令牌、云实例的凭据,以及 Kubernetes 服务账号的令牌。
GitHub 新规则管不到的地方
自 7 月 28 日起,GitHub 会把公开仓库中被判定为恶意的工作流运行拦下,直到协作者批准为止。Wiz 检查过的每一次公开 Gheist 运行,都卡在 action_required。“这项控制起了作用,恶意载荷从未运行。”
这项拦截只覆盖 github.com 上的公开仓库。在私有仓库和 GitHub Enterprise Server 上,Wiz 找到的审计日志显示 ci-sync-* 运行完成后随即被删除,CloudTrail 记录也显示组织的工作流从这些分支承担了 AWS 角色。
令牌来自开发者的机器
每个 Gheist 分支都需要一个有效的令牌,而 Wiz 把这些令牌追溯到窃密木马日志和泄露的构建工具。在开发者的笔记本电脑上,gh CLI 的登录、~/.git-credentials 里的个人访问令牌、shell 配置文件里的 GH_TOKEN:窃密木马一次就能全部复制走。编程代理让这类令牌更加普遍,因为一个会推送分支、开 pull request 的代理,随时都需要一个令牌在手边。
窃取也可能来自你信任的工具。10 月 19 日,一名控制某位 OVHcloud 开发者 GitHub 账号长达数月的攻击者,把 OVHcloud CLI 五个版本(v0.11.0 到 v0.15.0)的二进制文件换成了另一批构建,用 OVHcloud CEO Octave Klaba 的话说,这些构建会“从磁盘上取走机密”。BleepingComputer 报道,macOS、Linux 和 Windows 的构建全部受到影响。Klaba 的建议是:“如果你受到影响,请更换磁盘上所有的机密。”
被盗的令牌不必在同一天就落到 Gheist 这类攻击活动手里。窃密木马的日志会被转卖,而令牌在有人吊销之前一直都能用。
同一个令牌放在 Bromure 机器上
Bromure Agentic Coding 在你的 Mac 上以 Ubuntu 虚拟机运行 Claude Code、Codex、Grok、Kimi 和 omp。真正的凭据留在主机上,代理使用的是替身。在其中一台机器上,沿着 Gheist 的攻击链追踪一个 GitHub 令牌。
磁盘上的令牌是替身
在机器的 Credentials 中添加你的 GitHub 令牌。启动时,Bromure 会在虚拟机里写入一个假的:ghp_ 加 36 个字符,正是 gh 预期的格式,放在 GH_TOKEN、gh 的配置和 ~/.git-credentials 里。代理的 git push 和 gh pr create 照常工作。当请求离开虚拟机前往 github.com 时,Mac 上的代理服务器会把假的换成真正的令牌,而且只针对那个主机。
现在,在那台虚拟机里运行被植入木马的 CLI,就像代理照着某个项目的安装说明会做的那样。它读取磁盘,而上面每一个 GitHub 值都是替身。开发者身上其他常见的战利品也一样。~/.aws/config 指向一个会交出假私密密钥的辅助程序,代理服务器在 Mac 上用真正的密钥重新签署 AWS 请求。虚拟机里没有任何 SSH 私钥可读:SSH_AUTH_SOCK 通向主机上的一个 agent,它替虚拟机签名,但从不交出密钥。
Gheist 用不了替身
Gheist 工具在攻击者自己的基础设施上运行。它拿买来的令牌调用 api.github.com,期待看到一串仓库。换成 Bromure 的替身,GitHub 返回 401 Bad credentials。Bromure 用一个只留在你 Mac 上的盐值,从真正的令牌派生出替身,只有那台 Mac 的代理服务器知道它代表哪个真正的令牌。工具连一个仓库都列不出来,自然也不会创建任何分支。
Gheist 的整套手法,就是问一个令牌能触及什么。离开写入它的那台虚拟机,Bromure 的替身什么都触及不了。
发往窃密木马的替身会触发警报
窃密木马还得把找到的东西发到某个地方,而从 Bromure 虚拟机发出的每条 HTTPS 连接都会经过 Mac 上的代理服务器。代理服务器会扫描每个出站请求的请求头和请求体,找出被发往非原签发主机的替身。一个要发往 GitHub 以外任何地方的 ghp_ 替身,在任何一个字节离开 Mac 之前,就会被 HTTP 451 拦下。虚拟机随即暂停,并弹出 This environment may have been compromised 的警报,列出凭据名称、它所属的主机,以及它被发往的主机。
你可以关闭这台机器,或保存它的磁盘和主目录以供调查。被标记为已入侵的机器在磁盘和主目录清除之前不会再启动,所以窃密木马活不过重启。
在机器的 Guardrails 面板把 Unmatched traffic 设为 Deny,再放行项目需要的主机,窃密木马的上传主机就会被直接拒绝,不管传的是不是加密压缩包。被拒绝的连接会成为 Security Timeline 中的一行。
机器内的写入会等你
假设恶意软件放弃窃取,改在替身能用的地方使用它:从虚拟机内部,经过代理服务器。它就得做 Gheist 做的事:创建分支,提交工作流。
新的 Bromure 机器会把 GitHub 的写入策略设为 Prompt before write。读取照常通过。git push 或对 github.com 的 REST 写入,会停在你 Mac 上的一个对话框,原样显示这项操作:
Allow write on "GitHub" from workspace "web-app"?
POST /repos/<owner>/<repo>/git/refs
你正在看 pull request 时冒出一个你没要求的 ci-sync- 分支,要拒绝很容易。Don't allow 返回给代理的错误与阻止策略相同,而 Allow once 除了那一次写入,什么都不授权。
轮换只在一个地方进行
Wiz 的第一个响应步骤,是轮换受影响仓库能触及的每个机密,并吊销推送那些提交的令牌。“删除运行记录或分支,并不能撤销已经发生的泄露。”
在装有 Bromure 的 Mac 上,这次轮换中属于开发者的部分只是一个字段。真正的 GitHub 令牌存放在主机上单独一条加密条目里。把新的令牌粘贴进机器的 Credentials 面板,下次启动就会写入对应的替身;不必在 shell 配置文件、.env 文件或代理配置里四处搜寻副本。对于以 1Password 引用方式保存的密钥(Other API keys 下的 op://vault/item/field),Bromure 每两分钟重新解析一次,所以在 1Password 里的轮换无需重启就会送达正在运行的机器。
保险库本身由 macOS Data Protection Keychain 中的一把密钥保护,这把密钥绑定 Bromure 的签名身份,且从不同步。把 Bromure 文件夹从 Mac 复制走的窃密木马,拿到的只是密文。
让代理去排查,只读
Wiz 的排查步骤是跨越大量仓库的繁琐工作,正好适合交给代理:列出所有匹配 ^ci-sync-[0-9a-z]{6}$ 的分支,找出只添加 sync-check.yml、提交信息为 ci: sync config 的提交,再从审计日志中捞出完成后不到一分钟就被删除的运行,以及 user agent 为 gheist 的请求。
把这份工作交给一台 GitHub 写入策略设为 Read-only 的 Bromure 机器。代理可以读取令牌允许的每个仓库和审计日志,而任何推送或 REST 写入都会在到达 GitHub 之前,就在 Mac 上被拒绝。如果代理为了查看而检出一个 ci-sync- 分支,包括 Wiz 看到被重新推送、夹带 PolinRider 加载器的那些分支,运行起来的任何东西都是在一台满是替身的虚拟机里运行。每一次凭据替换都会在 Security Timeline 中成为一行 Credential brokering,事后你就能看出这次排查碰过哪些主机。
窃密木马找到的是替身
GH_TOKEN、gh 的配置和 ~/.git-credentials 里放的是假的 ghp_。真正的令牌加密留在 Mac 上。
Gheist 拿到 401
离开这台机器,替身就不是 GitHub 令牌。工具列不出任何仓库,所以什么也植入不了。
泄露会暂停虚拟机
要发往 GitHub 以外任何地方的替身会被 451 拦下,虚拟机会一直暂停,直到你决定如何处置。
写入先问过你
新机器在每次推送和每次对 GitHub 的 REST 写入之前都会先询问,屏幕上显示确切的请求。排查可以以只读方式运行。
把令牌从笔记本电脑上拿走
GitHub 的批准拦截在公开仓库上起了作用,而 Wiz 的加固清单也是对的:生产环境必须有审查者、OIDC 信任限定在受保护的分支、用 GitHub Apps 或短期的细粒度令牌取代个人访问令牌。这些都能缩小被盗令牌落入他人手中后所能做的事。
Bromure Agentic Coding 则在更早一步发挥作用。你的代理整天推送分支、开 pull request,而它们使用的令牌从不放在窃密木马读得到的磁盘上。Gheist 只能用仍然有效的令牌植入工作流,而 Bromure 机器上的令牌,只有经过那台机器自己的代理服务器才有效。
想知道同样的替身如何抵挡一个专为编程代理文件打造的窃密木马,请阅读《窃密木马打包带走的是替身》。想看一个被盗的 GitHub 令牌如何导致一次被投毒的发布,请阅读《代理的令牌是组织所有者的》。