蠕虫开源了
2026 年 5 月 11 日那一周的某个时刻,Shai-Hulud 背后的人——这条自 2025 年 9 月以来一直在吞噬维护者账号的自复制 npm 供应链蠕虫——把自己的源代码泄露了。到了周末,OX Security 在同一个账号下发现了四个 typosquat npm 包,其中一个几乎是泄露蠕虫的逐字拷贝,另一个是 Golang DDoS 僵尸程序,剩下两个是把 SSH 密钥和加密货币钱包发往廉价 C2 的普通信息窃取器。供应链攻击的复刻门槛刚刚被大幅拉低,而最有可能装上这些包的人,已经不再是人。
上周的某个时刻,Shai-Hulud 背后的人——那条自 2025 年 9 月以来一直
在啃食维护者账号的 npm 蠕虫——把自己的源代码泄露了。到了周末,一个
叫 deadcode09284814 的 npm 账号已经发布了四个复用这段代码的
typosquat。其中一个就是蠕虫本身,几乎一字不差。一个是 Golang DDoS
僵尸程序。两个是普通信息窃取器,会把你的 SSH 密钥、你的
~/.aws/credentials 和你的 MetaMask 钱包 POST 到一个租来的 IP。
在两千六百七十八次安装之后,问题已经不再是“会不会有人把这次泄露
武器化”。问题是:今天下午在你终端里敲 npm install 的那些 agent
中,有几个会顺手把它们装进来。
在攻击性软件领域,周期性地会发生这样的事:某个封闭工具被丢上某个
论坛,能用上它的人群,从“写它的那一支队伍”一下子变成了“任何一个
有 VPS 的少年”。Mimikatz。EternalBlue。Conti 的源代码。每一次,
能力本身并没有变化——只是不再稀缺了。上周的头条新闻,由
BleepingComputer
基于
OX Security
的研究做了报道:Shai-Hulud——也就是我们之前
写过的那条六分钟内吞下四十二个
@tanstack 包的蠕虫——也加入了这份名单。
复刻并非空谈。到了周日,OX 已经记录到从同一个名为
deadcode09284814 的 npm 账号发布的四个恶意包:
chalk-tempalte——chalk-template的 typosquat,携带了泄露的 Shai-Hulud 源代码的近乎一字不差的拷贝,包括 Shai-Hulud 把窃取 到的凭据上传为公开的自动生成 GitHub 仓库这一标志性行为。OX 的 判断是:“这个 Shai-Hulud 恶意代码几乎是泄露源代码的精确拷贝, 没有任何混淆手法”,说明这是一个连序列号都懒得磨掉的新威胁行为 者。axois-utils——axios-utils的 typosquat,投递的是 OX 称之为 Phantom Bot 的 Golang payload:HTTP、TCP、UDP 以及 reset 洪水 攻击,通过 Windows 启动文件夹和计划任务实现持久化,C2 位于b94b6bcfa27554.lhr.life。你的开发机,被征召入伍。@deadcode09284814/axios-util——又一个 typosquat,又一种 payload:SSH 密钥、环境变量、AWS/GCP/Azure 凭据,统统发往80.200.28.28:2222。用 OX 的话说,“相当直白”。color-style-utils——一个简单的信息窃取器,抓取你的 IP、 地理位置、加密钱包数据并 POST 到edcf8b03c84634.lhr.life。
OX 撰写这份报告时,这四个包的合计周下载量是 2,678。
这里其实有两个故事缠在一起,值得分开看。无趣的那个是:npm 上有
typosquat。它一直有;它将来也一直会有,原因和狗公园里有一堆叫
Bench 的狗一样——名字便宜,命名空间又是扁平的。有趣的那个是:
运行一条 Shai-Hulud 级别的蠕虫的门槛刚刚被拉低了。直到上周,
你还需要原班人马的工具链、原班人马的基础设施,以及原班人马那种
不被抓到的纪律性。今天,你只需要一份公开仓库的 GitHub 克隆、一条
lhr.life 隧道,以及敲下 npm publish 的耐心。OX 找到的这四个
包,合起来就是证明。
为什么行为者数量比包数量更重要。
一条单独的、复杂的蠕虫,在某种意义上是个可处理的对手。它有破绽。
它有基础设施。它有习惯。可以为它编写检测特征。Aikido、Socket、
Snyk、Wiz——这些跳上 5 月 11 日 @tanstack 事件的 npm 供应链
监控厂商——在几个小时之内就抓到了它,正是因为他们已经盯着同一
家族盯了八个月。
一个由从粘贴站点上下载源代码的人写出的衍生蠕虫家族则是另一种
形态。每一条都会渗漏到不同的 C2,嵌入不同的 RSA 密钥,选择不同
的文件组合去读,并选择不同的 typosquat 空间安家。有些会很小心;
大多数会犯下被迅速抓住的低级错误;其中某一条——下一次我们写这种
帖子的时候——会精巧到一周之内我们都抓不到它。防御者的检测问题
从“认出 Shai-Hulud”扩大到了“认出任何想从 prepare 脚本里读
~/.ssh/id_ed25519 的东西”。这是一个大得多、大得多的面。
安装路径的形状也变了,而这正是任何在跑编码 agent 的人都应该担心
的部分。一个在 2024 年想要 chalk-template 的人类开发者,会从
某个教程里读出这个包名,敲进去,然后在 chalk-tempalte 返回的
是“两百次下载,发布者是个陌生人”时注意到拼错了。一个在 2026 年
被要求“给我的 CLI 输出加点颜色”的编码 agent
则会装上包管理器扔回来的任何东西。agent 不看发布者字段。agent
不会注意到 README 只有三行。agent 这个小时里要跑三十次
npm install,因为用户在干一支小团队的活儿,而 agent 是按任务
而不是按安装计酬的。
一份未经混淆的 Shai-Hulud 克隆到底做了什么。
chalk-tempalte 这个包,用 OX 的话说,“几乎没有任何修改”地复刻
了泄露的源代码。这意味着我们在
那条把自己写进 .claude 的蠕虫
里走过的那套机制照样适用,只多了一个重要的新花样:渗漏通道就是
GitHub 本身。
Shai-Hulud 的标志性手法——被这个仿冒者保留下来,因为复制比重写
容易——是把窃取到的凭据并不送到一台隐藏的接收服务器。它们被
送进一个新建的公开 GitHub 仓库,这个仓库是用恶意代码刚从受害者
那里偷来的 GitHub token 发布的。受害者的机密就这么躺在一个由
受害者自己的 GitHub 账号拥有的公开仓库里,任由世界上的任何人去
抓取,直到有人注意到,仓库被清除掉为止。防御方的标准战术手册——
封锁渗漏域名,寻找异常的出站 DNS——抓不到这一招,因为出站 DNS
查询的是 api.github.com,你的开发机本来就一小时跟它说两百次话。
凭据包以一次 git push 的伪装,离开了你的笔记本。
一旦机密被公开,任何盯着 GitHub 实时流的人——而确实有好几拨人 正是为了这个原因盯着 GitHub 实时流——就能把它们捡走。蠕虫不需要 让自己的 C2 保持在线。GitHub 就是 C2。
这张图里有个小细节值得停下来多看一眼。Shai-Hulud 克隆,也就是
chalk-tempalte,甚至不靠自己的基础设施来接收战利品。它使用
受害者自己的 GitHub 身份,把受害者自己被偷的机密,发布到
GitHub 上的一个公开仓库里。lhr.life 上的那个 C2 只是备份。
主要通道是 git push。对一个盯着出站流量的防御者来说,这与开发
者正常的 CI 完全不可区分。对 GitHub 来说,这——直到有人举报这个
仓库为止——是一个由真实用户拥有的合法公开项目。渗漏的过程被
受害者自己的身份洗白了。
你滑过这段文字的时候,agent 正在干什么。
如果你的终端里有一个编码 agent——Claude Code、Cursor's CLI、
Codex CLI、Aider,你挑一个——那么在你读完上一段的这段时间里,
agent 很有可能已经替你跑过一次 npm install。也许两次。编码
agent 不会停下来欣赏依赖树。你买它,就是因为它不会停。
OX 抓到的那些包是 typosquat,这是一类极其适合机器速度的错误。
一个必须亲手敲 chalk-template 的人,会把字母顺序敲对,因为
他敲过一百次了。一个吃下了地球上每一篇 Stack Overflow 帖子的
模型,在同一份训练语料里看到过 chalk-template 和
chalk-tempalte——后者通常出现在别人犯错的截图里——给一个像
“给我的 CLI 加上彩色输出”这样的 prompt,有时候会原封不动地把
拼错的版本吐出来。agent 不会眨眼。包管理器不会眨眼。prepare
脚本就开跑了。
这不是一种假想的失败模式。这正是 Shai-Hulud 家族被设计来攻击的 那种失败模式。最初的蠕虫通过窃取维护者 token,然后用这些 token 重新发布那些合法热门包的更多被污染版本来传播。仿冒者还没有拿到 维护者的 token;他们手里的是 typosquat 命名空间,而 agent 恰好 极其擅长一脚踩进去。
Bromure 在这个故事里站在哪儿。
Bromure Agentic Coding 的配置方式是:编码
agent 跑在一个每任务一次性的 Linux 虚拟机内部,项目文件夹
被挂载进去,出站流量由代理中转,凭据被保留在 hypervisor macOS
宿主侧。这套架构我们在
Bitwarden CLI 那篇文章
和
@tanstack 那篇文章
里都详细走过一遍。下面是在这个边界内,这四个包各自具体会发生
什么。
把这套架构对着这四个包逐一过一遍,因为细节正是这个架构赚回工钱 的地方。
chalk-tempalte 去摸 $GH_TOKEN。
Shai-Hulud 克隆的标志动作,是拿走开发者的 GitHub token,然后用它
在开发者自己的账号下创建一个公开仓库。在 Bromure VM 内部,它读到
的 $GH_TOKEN 是一个桩——一个语法上合法、以 ghp_ 开头、专门
为这一刻存在的字符串。运行体的第一个动作是向 api.github.com
发起 POST /user/repos。宿主侧的出口代理把 api.github.com
识别为白名单端点,但仅限于当前任务实际请求过的操作——git push
到这个任务正在改的仓库,gh pr create 到同一个仓库,
gh api repos/that/repo/issues。“在用户账号下新建一个公开
仓库”不在那张列表上,因为用户没有提这个要求。代理拒绝把桩 token
替换成真 token,桩值就这么作为桩值发出去了。GitHub 返回 401。
蠕虫的渗漏通道——那个聪明的、为绕过 DNS 出口过滤而设计的
通道——从未打开。
备用通道,也就是 87e0bbc636999b.lhr.life 上的 lhr.life
隧道,同样不在白名单里。trace 把这次尝试记录在案。数据字节并没
有离开。
axois-utils 安装 Phantom Bot 进行持久化。
那个 Golang 僵尸尝试把自己写进 Windows 启动目录并创建一个计划
任务。Bromure VM 是 Linux 客户机,所以 Windows 特定的持久化
免费就是空操作。在同一 payload 的 Linux 变种里——很快就会有人
做出来——僵尸会把自己写进 /etc/cron.d/ 或
~/.config/systemd/user/。这两个路径都在客户机一次性的
copy-on-write 磁盘里。下一次 bromure reset,或者当前任务自然
结束,都会把这块磁盘扔掉。持久化没了,根本不用去猎杀。
与此同时,僵尸对外连接 b94b6bcfa27554.lhr.life 也不在本任务
的出口白名单里,因为没有哪个正经编码任务会去跟一条刚注册的
lhr.life 隧道说话。僵尸朝着一个关闭的 socket 报到了。会话
trace 把这次尝试记录下来——这在明天早上 IOC 列表公布的时候会
派上用场。
@deadcode09284814/axios-util POST 原始凭据。
这四个 payload 里最简单的那个,恰好也是能抓到的东西最少的那个。
运行体读取客户机的 ~/.ssh、~/.aws、环境变量,然后 POST 到
80.200.28.28:2222。SSH 目录是空的。AWS 文件是个桩。环境变量
要么是桩要么根本没设。目的 IP 不在白名单。要么连接在代理处就被
阻断,要么它带着一堆占位符离开宿主。两种结果都没事。
color-style-utils 去找根本不存在的钱包。
这个加密货币窃取器,是四个包里威胁模型最明确假设开发者自己的
浏览器和它在同一台机器上的一个。它读取像
~/Library/Application Support/Google/Chrome/Default/Local Extension Settings/<MetaMask-id>/
这样的路径,以及 Phantom 和 Keplr 的对应路径。这些路径在 Bromure
VM 里一个都不存在。VM 里没有你的 Chrome 配置。VM 里没有装钱包
扩展。VM,本来就被设计成不是任何人的主力浏览器。运行体打开一
个空目录,然后转身走了。
这一部分不是一个关于凭据存放在宿主上的故事。钱包之所以在宿主 上,是因为在一台正常的笔记本上,开发者的浏览器和开发者的编码 agent 共用一个文件系统。Bromure 没有把钱包变得更安全;它只是让 钱包对蠕虫正在运行的那个地方变得不可达。蠕虫读不到不在它磁盘上 的东西。
还有哪些地方仍然会疼。
这个故事里有几个角落,Bromure 的每任务 VM 并不算是修复,值得把 它们大声说出来。
项目文件夹是挂载进来的。
蠕虫写进项目文件夹里的文件——包括我们在 @tanstack 那篇文章
里讲过的那种 .claude/router_runtime.js 式的持久化——在任务
重置后依然在,因为挂载项目文件夹本身就是为了这个目的。这里的
防御不是 VM。是 git status,以及在 push 之前花五秒钟看一下
diff。trace 让人更容易看出来,是哪个会话加进了不该出现的
文件。
出口白名单是故意收得窄的。
Bromure 的凭据中转之所以管用,正因为白名单很窄。如果你因为
今天要发版而把 npm publish 加进了自己 scope 的白名单,而你
今天碰巧装上了这四个包之一,蠕虫就会以你的 scope 发布。白名
单只放任务真正需要的东西。一个字节都不多。
为本任务发出的 token 仍然是真 token。
桩 GitHub token 在线路上会被换成真 token,仅限于任务已经加进
白名单的操作。如果 chalk-tempalte 能把 agent 哄去对项目自己
的仓库做一次 git push,那次 push 就会以真 token 通过。边界
保护的是凭据。它不审 diff。请你审 diff。
检测在 trace 下游。
会话 trace 会记录每一条 shell 命令、每一次文件写入、每一次
出站请求。它本身并不会把 87e0bbc636999b.lhr.life 判定为
坏。它只是记下这个请求发生过。等明天早上 OX 公布一份新的 IOC
列表,你的搜索只需要两秒。这才是 trace 带来的价值——不是魔法,
只是凭据。
最后还有一件事。
这次泄露本身不是新闻。新闻是这次泄露在接下来一年里会让什么事变
得很可能发生:大量小规模、半吊子的蠕虫复刻,而即使是它的精巧
原形,npm 生态也只是勉强及时抓住。其中一些复刻会响到足以拿到
一篇报道。大多数会在 registry 里躺一个星期,收上几千次
npm install,然后等到有人终于提交滥用举报后消失。OX 在
deadcode09284814 这几个包上看到的两千六百七十八次安装并不是
异常值。那是平均值。
诚实的问题不是“我的团队能不能避开 npm 上每一个有毒的 typosquat”。 agent 敲得飞快。名字又便宜。诚实的问题是:当 agent 装上一个的 时候——而在接下来一年里,在一支使用 agent 的团队中,这一定会 发生——post-install 脚本找到的,是开发者的双手,还是一台在凭据 文件里装着桩值、并配有一个不认识自己是谁的代理的、一次性 Linux 盒子。
Bromure Agentic Coding 是第二种。它免费、 开源,而且今天就已经在用。复刻树会在变好之前先变得更糟。