吊销令牌,就是引爆点
10 月 8 日,tensorlake 0.5.144 带着一只 Shai-Hulud 蠕虫上了 npm。它窃取 GitHub、npm、云和 SSH 凭据,在受害者的仓库里植入 .claude/settings.json,并安装一个监控程序,每 60 秒检查一次被盗的 GitHub 令牌,一旦令牌失效就执行 rm -rf ~/。吊销令牌,也就是标准事件响应流程的第一步,会抹掉受害者的家目录。在 Bromure Agentic Coding 里,默认的存在时间闸门让 npm 看不到这个版本,而机器里的 GitHub 令牌从来就不是真的。
蠕虫在代码里给发现它的人留了一串字: “IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner”。意思是:吊销这个令牌, 就会抹掉主人的电脑。代码说到做到。
10 月 8 日星期三 01:12 UTC,tensorlakeai/tensorlake 的发布工作流把 tensorlake
SDK 的 0.5.144 版发布到了 npm。Tensorlake 是面向 AI 智能体的无服务器沙箱平台,它的 npm
包每周约有 12,000 次下载。新版本来自项目自己的仓库,由项目自己的工作流构建,npm 上还显示了
它的来源证明(provenance attestation)。
Socket 在 11 分钟后标记了它。 StepSecurity、 Aikido 和 The Hacker News 当天也相继跟进。这个版本携带 Shai-Hulud 蠕虫的一个新变种,而这一只还会扣押人质。
它是怎么进来的
StepSecurity 还原了时间线。10 月 7 日 01:20 UTC,有人以一位维护者的名义,直接把一个 commit 推到了仓库的主分支。接下来几个小时里又来了七个 commit。没有人发现,一天后发布工作流 构建了 main 上的代码并把它发布出去。来源证明是准确的。它证明了包是在哪里构建的,而那个地方 早已落入攻击者手中。
包的 package.json 多了一行:
"preinstall": "node lib/setup.mjs"
npm 在包被解压到位之前就会执行 preinstall。这个脚本下载 Bun 运行时,再用它运行
lib/Math_Symbol.js,一个 856 KB 的混淆载荷。在 CI 上它会跳过自己。StepSecurity 的总结是:
“目标是开发者的机器。”
它拿走什么,发往哪里
载荷会收集 GitHub 和 npm 令牌、AWS、Azure 和 GCP 的云密钥、Kubernetes 和 HashiCorp
Vault 的机密、SSH 密钥、.env 文件、浏览器保存的登录信息、加密货币钱包,以及 AI 编程工具的
配置:.claude、.cursor、.kiro、Windsurf 和 Zed。它把战利品加密后发往
iseekaigogo.com;发不出去的话,就在受害者自己的 GitHub 账号里创建一个公开仓库放进去,描述
写着“Shai-Hulud: Here We Go Again.”。域名被拿下时,它会通过大约 30 个公共 RPC 端点,到一个
以太坊合约里查找新域名。
接着它开始扩散。拿着受害者的 npm 令牌,它下载受害者的每一个包,把自己加进去,升版本号,
重新发布。拿着 GitHub 令牌,它以 [email protected] 的身份,用
chore: update dependencies 这条提交信息,把 .claude/settings.json 和
.vscode/tasks.json 提交到受害者的仓库。下一个在 Claude Code 或 VS Code 里打开这些项目的
开发者,会再运行一次蠕虫。正如 StepSecurity 的 Ashish Kurmi 所说,这个恶意软件“在有人用
Claude Code 或 VS Code 打开项目时会再次运行”。
人质
新东西是一个叫 gh-token-monitor 的服务。在 Linux 上,它是
~/.config/systemd/user/ 里的 systemd 用户单元;在 macOS 上,是位于
~/Library/LaunchAgents/com.user.gh-token-monitor.plist 的 LaunchAgent;在 Windows
上,是一个运行 monitor.ps1 的登录任务。StepSecurity 描述了它的行为:
每 60 秒,最长 24 小时,该服务会拿那个令牌去请求 GitHub API。如果 GitHub 拒绝它, 服务就执行
rm -rf ~/。
假设你是受害者。你得知自己装的某个包偷走了你的 GitHub 令牌。标准流程说要立刻吊销。你吊销了,
不到一分钟,你的家目录就没了:你的 checkout、还没提交的工作、dotfiles,以及蠕虫干过什么的
证据。Socket 在代码里找到了以字面字符串写明的威胁:
IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner。
所以现在的安全公告都附带了一条顺序规则。StepSecurity 的第 3 步是找到并移除监控程序,并且 先备份所有不能丢的东西。它用粗体警告:“在完成第 3 步之前,不要吊销任何 GitHub 令牌。” Socket 也这么说。你花在搜寻监控程序上的每一个小时,攻击者都握着一个带你 push 权限的有效 令牌。
在 Bromure 机器里做同样的安装
在 Bromure Agentic Coding 里,Claude Code、Codex 和其他代理都运行在你 Mac 上的 Linux VM
里,一台机器一个 VM。VM 打开的每一条连接,包括每一次 npm 下载,都要经过 Mac 上的代理服务器。
在其中一台机器里重演 10 月 8 日:代理把 tensorlake 加进项目,然后执行 npm install。
那个版本还不存在
每台新机器都默认开启供应链的存在时间闸门,最少两天。当 npm 向注册表请求 tensorlake
的版本列表时,代理服务器会去掉所有发布不到两天的版本,并让 latest 指向足够旧的最新版本。
10 月 8 日那天,就是 0.5.143。npm 装了它便继续往下走。从机器内部看,0.5.144 从未存在过,
等到它的时间超过门槛时,它早已从 npm 上消失了。
如果 lockfile 或代理点名要 0.5.144,代理服务器会用 HTTP 451 拒绝下载,并附上一段 npm 会 原样打印的理由:
Bromure Supply-Chain Security blocked this request: npm package
[email protected] published 3 hours ago — policy requires 2 days minimum
代理也会读到这一行,并自行锁定上一个版本。
同一个 Supply Chain 面板里还有两层。选中 socket.dev 并开启
Block compromised packages 时,只要 socket.dev 标记了该包,下载就会被拒绝;10 月 8 日
Socket 只用了 11 分钟。开启 Strip preinstall / install / postinstall / prepare from npm
tarballs on the fly 时,代理服务器会改写 tarball,"preinstall": "node lib/setup.mjs"
这一行在 npm 看到之前就消失了。这一行是蠕虫唯一的入口。
假设它还是运行了
把每一层都拿掉,让载荷运行。它以 VM 用户的身份,在这台机器自己的家目录里运行,开始寻找清单 上的东西。
令牌都是替身。 VM 里没有任何文件、环境变量或进程持有真正的 GitHub 令牌、云机密或 SSH
私钥。GH_TOKEN 和 ~/.git-credentials 里放的是格式正确的 ghp_ 令牌,Mac 上的代理
服务器只在请求要发往 GitHub 时才换上真的。~/.ssh 里没有私钥:SSH_AUTH_SOCK 是通往 Mac
上 ssh-agent 的桥接,ssh-agent 只负责签名,从不交出密钥。你 Mac 的家目录、钥匙串和浏览器
配置文件根本不在 VM 里。蠕虫加密并发出的是一包替身,除非经过那台代理服务器,GitHub、AWS 和
Kubernetes 在任何地方都会拒绝它们。
出去的路要经过 Mac。 在 Guardrails 面板里把 Unmatched traffic 设为
Deny,并为模型提供商、注册表和 GitHub 配好规则的机器,永远不会连到
iseekaigogo.com,也不会连到那 30 个以太坊 RPC 端点中的任何一个。无论设置如何,这些连接
都会出现在 Security Timeline 里,它在每台机器上都始终开启。
写入要等你点击。 新机器对每个 git 托管平台都使用 Prompt before write。蠕虫在
GitHub 上的后备方案是在你的账号里创建公开仓库:那是一个 POST /user/repos,属于写入,所以
macOS 会显示 Allow write on "github.com" from workspace "…"?,附上请求在线路上的原貌。
它扩散时会把 .claude/settings.json 提交到你的仓库并推送:同一个对话框,这次附的是
POST /acme/app.git/git-receive-pack。你没让代理创建一个叫“Shai-Hulud: Here We Go
Again.”的仓库。你点下 Don't allow,蠕虫的扩散就在你的 Mac 上结束了。
人质握着的是替身
gh-token-monitor 单元照样会把自己装进 VM 的 ~/.config/systemd/user/,并开始每 60 秒
用它找到的令牌调用 api.github.com/user。那个令牌就是替身。代理服务器为 GitHub 换上真的,
GitHub 返回 200,监控程序继续等。
这把响应的顺序倒了过来。在笔记本上,真令牌在攻击者手里,比的是在对方使用之前吊销它,又不能
触发抹除。在 Bromure 里,真令牌从没进过 VM,攻击者手上没有东西可以跟你赛跑。除了你的代理
服务器,没有任何地方接受他拿走的那个 ghp_ 值。
把机器关机,监控程序就跟着停了。打开 Rewind home…,选择安装之前那次启动的检查点:
Bromure 每次成功启动都会创建一个。单元文件、~/.local/bin 里的脚本,以及植入机器 checkout
里的 .claude/settings.json 文件,全都回到原来的样子。片刻之前的家目录会先保存为一个新的
回滚点,所以回退本身也可以撤销。想要从头来过,Reset disk 会从签名过的基础镜像重新克隆
系统磁盘。
就算开关还是触发了,rm -rf ~/ 删掉的也只是一个 VM 里的 /home/ubuntu。你 Mac 的家目录
不在它的范围之内,而 Rewind home… 能从最后一个检查点把机器的家目录找回来。
是谁装了它?
遇到这类事件,第一个问题是团队里有没有哪台机器拉取了那个坏版本。在笔记本上,你得翻 lockfile、npm 缓存和 shell 历史记录,再祈祷蠕虫没有清掉自己的痕迹。
在 Bromure 里,无论有没有开启任何供应链防护层,每一次包下载都是 Security Timeline 里的
一行:npm · [email protected],附带机器、时间和判定。这些记录在 Mac 上保存 90 天,VM
里的任何东西都改不了。用 Supply chain 筛选、搜索那个版本,答案就有了。Export… 能把
它存成 CSV 附到事件工单里。在注册到 bromure.io 的 Mac 上,同一次下载还会以
supply_chain.fetch 事件进入组织的事件流,让安全团队一次就能替所有开发者问这个问题。
坏版本从没出现过
存在时间闸门默认开启、最少两天,会从 npm 的版本列表里去掉太新的版本。10 月 8 日,
npm install tensorlake 解析到的是 0.5.143。
被盗的令牌是替身
GitHub、npm、云和 SSH 凭据都留在 Mac 上。蠕虫扣押的人质,是一个只有经过 Bromure
代理服务器才有效的 ghp_ 值。
扩散需要你点头
创建仓库和推送 commit 都是写入。在新机器默认的 Prompt before write 下,两者都要等一个 VM 无法回应的 macOS 对话框。
抹除只影响一个 VM
rm -rf ~/ 碰得到的是机器的家目录,不是你的。Rewind home 会从上次启动时创建的检查点
把它恢复。
下一套流程
Shai-Hulud 今年一再卷土重来,它的作者会按照防守方上一次的处理方式来设计每个新版本。防守方 几个小时内就吊销令牌,所以这一版惩罚吊销。下一版会惩罚别的事:运行清理脚本、删除 LaunchAgent、再次打开项目。
在笔记本上,这些花招每一个都管用,因为攻击者在你正要清理的同一台机器上握着真东西。在 Bromure 机器里,真正的凭据、你的家目录和事情经过的记录,全都在 Mac 上,在代理服务器后面。 清理就是回退,而且你可以在轮换任何东西之前就完成。想进一步了解背后的线路边界,请阅读 安装过程很干净 和 每次逃逸四万美元。