CHAINDROP:当 npm 蠕虫学会了自我复制
漏洞分析
2026 年 8 月,Elastic Security Labs 披露了 npm 供应链蠕虫 CHAINDROP,该蠕虫通过 postinstall 钩子窃取开发者 npm Token,再利用 Token 自动投毒受害者维护的所有包,形成指数级传播,短时间内感染超过 400 个 npm 包。本文从技术实现角度深入剖析蠕虫的攻击机制,在隔离环境中复现关键环节,并探讨多层防御思路。
npm 供应链蠕虫 CHAINDROP 深度剖析:自我复制式攻击的技术本质与防御思考 ========================================== > **作者**:dream123 **日期**:2026-08-13 **分类**:安全研究 / 供应链安全 **声明**:本文仅供安全研究与防御学习使用,请勿用于非法用途。 一、事件背景 ------ 2026 年 8 月 6 日,Elastic Security Labs 披露了一起 npm 供应链蠕虫攻击事件——**CHAINDROP**。据报道该蠕虫感染了超过 400 个 npm 包,核心手法是通过 `postinstall` 钩子窃取 npm Token,再利用 Token 自动投毒受害者维护的其他包,形成指数级传播。 不过说实话,"400 个包"这个数字看起来吓人,但仔细想想,npm 上有数百万个包,400 个连万分之一都不到。而且这 400 个包里有多少是真正有用户在用的、有多少是无人问津的"僵尸包",报告里并没有说清楚。**我怀疑真正受影响的用户数量,远没有标题党们渲染的那么夸张。**  第一次看到 CHAINDROP 的技术细节时,我的第一反应不是"这个攻击好厉害",而是"这个攻击怎么现在才出现"。因为从技术实现的角度来看,CHAINDROP 没有任何创新——它用到的每一个能力(读文件、调 API、发 HTTP 请求、bump version、npm publish)都是 npm 生态中最基础的功能。它只是把这些功能按正确的顺序串起来了而已。 这才是最让人不安的地方:**攻击的门槛低到只需要把已有的积木拼起来,而防御的门槛却高到需要改造整个生态。** 本文将从技术实现的角度,深入分析 CHAINDROP 的攻击机制,在隔离环境中复现关键环节,并探讨防御思路。 和以往的供应链投毒不同,CHAINDROP 有一个本质区别:**它不需要攻击者持续参与**。 但这里我要提一个问题:**CHAINDROP 真的算"蠕虫"吗?** 传统意义上的蠕虫(如 Conficker、WannaCry)是通过网络漏洞自主传播的,不需要用户任何操作。而 CHAINDROP 的传播仍然依赖于"受害者主动安装恶意包"这个前提——如果没人安装,它就传播不了。从这个角度看,它更像一个"自动化投毒脚本",而不是严格意义上的蠕虫。当然,叫什么名字不重要,重要的是它的攻击模式确实是以前没有过的。 传统的供应链投毒是这样的:攻击者找到一个目标包,想办法注入恶意代码,然后等待受害者上钩。每投毒一个新包,攻击者都需要手动操作。这就像往河里扔毒药——一次只能毒一片水域。 CHAINDROP 不一样。它把"受害者"变成了"攻击者"——蠕虫利用受害者的 npm 凭证,自动去投毒受害者维护的其他包。攻击者只需要投下第一颗种子,之后的传播完全自动完成。 这里有一个很多人忽略的细节:**CHAINDROP 利用的不是代码层面的漏洞,而是信任层面的漏洞。** npm 的设计假设是"如果你有 Token,你就是合法的发布者"。这个假设在正常情况下是成立的——Token 是你登录时生成的,只有你知道。但当 Token 可以被第三方代码(postinstall 脚本)轻易读取时,这个假设就崩塌了。 换句话说,CHAINDROP 不是利用了一个 bug,而是利用了一个 design choice。这让修复变得格外困难——你不能靠打补丁来解决,你需要重新设计。 - - - - - - 二、三个致命的设计缺陷 ----------- CHAINDROP 能成功,不是因为它的技术有多高明,而是因为 npm 生态系统中存在三个长期被忽视的设计缺陷。 ### 2.1 postinstall:一个被严重低估的攻击面 npm 的 `scripts` 字段允许包定义生命周期脚本,其中 `postinstall` 会在 `npm install` 完成依赖安装后**自动执行**。  这里的关键是:`postinstall` 脚本拥有**当前用户的完整权限**。它可以读写文件、执行命令、发起网络请求。而且 npm 不会显示即将执行的脚本内容,也不会询问用户是否允许——它只是默默执行,然后显示一个绿色的 ✓。 这个设计在 npm 的早期是有意义的——很多包需要在安装时编译原生模块(比如 `node-sass`),`postinstall` 提供了必要的灵活性。但在今天,这个机制已经成为供应链攻击最理想的载体。 我个人的一个观察是:**postinstall 的安全问题之所以长期被忽视,是因为它造成的伤害是"延迟的"。** 你今天 `npm install` 了一个包,postinstall 脚本在后台读走了你的 Token,但你什么感觉都没有。真正的伤害发生在几天甚至几周后——当蠕虫用你的 Token 投毒了你维护的包,你的用户开始被感染。这种"延迟爆炸"的特性,使得攻击链很难被追溯,也很难被用户感知到。 ### 2.2 npm Token:明文存储的万能钥匙 开发者执行 `npm login` 后,Token 以明文形式存储在 `~/.npmrc` 文件中: //registry.npmjs.org/:\_authToken=npm\_abc1234567890... 没有加密,没有权限保护,没有过期时间。任何有文件读取权限的进程都能读到它。 这个 Token 是一个"万能钥匙"——拥有它就可以以该开发者身份发布新版本的包、修改已发布包的元数据。而且 npm 不会向开发者发送 Token 使用的通知——即使 Token 被盗用,开发者通常也不会察觉,直到有人报告他的包被投毒。 这里有一个反直觉的事实:**npm Token 的安全性,取决于你安装过的所有包的 postinstall 脚本的善意。** 你信任 A 包,所以你安装了它。A 包的 postinstall 脚本可以读取你的 Token。如果 A 包的维护者是恶意的(或者 A 包本身被投毒了),你的 Token 就泄露了。而你的 Token 可以用来发布 B 包、C 包——这些包的用户又信任你。 **信任是可以传递的,但风险也是。** 这是供应链安全最根本的矛盾。 ### 2.3 包发布机制:缺乏行为监控 npm Registry 的发布机制非常"自由":只要你有有效的 Token,就可以随时发布任何你名下的包的新版本。没有发布频率限制,没有异常行为检测,没有二次确认。 我曾经想过一个问题:为什么 npm 不对异常的批量发布行为发出告警?比如同一个 Token 在 10 分钟内发布了 20 个包的新版本,这明显不正常。 后来我想明白了:**npm 的商业模式依赖于"发布便利性"。** 如果加了太多安全限制(发布前要审核、异常行为要告警、批量发布要二次确认),开发者会觉得麻烦,可能会转向其他包管理器。npm 在便利性和安全性之间,选择了便利性。 这个选择在 npm 的早期是合理的——那时候包的数量不多,社区规模不大,信任成本低。但当 npm 拥有数百万个包、数十亿次周下载量时,这个选择的代价就变得越来越大。CHAINDROP 就是这个代价的最新体现。 - - - - - - 三、逆向蠕虫 payload:四步完成自我复制 ----------------------- CHAINDROP 的蠕虫 payload 大约 100 行 Node.js 代码,没有任何高级技巧。它的工作流程分为四步:  **第一步:偷钥匙。** 蠕虫读取 `~/.npmrc` 文件,用正则提取 `_authToken` 字段: const npmrc \\= fs.readFileSync(os.homedir() + '/.npmrc', 'utf8'); const token \\= npmrc.match(/\_authToken=(.+)/)?.\[1\]?.trim(); 为了兼容不同的 npm 配置格式,蠕虫使用了多个正则模式来匹配: const patterns \\= \[ /\\/\\/registry\\.npmjs\\.org\\/:\_authToken=(.+)/, /\_authToken=(.+)/, /\\/\\/registry\\.npmjs\\.org\\/:\_password=(.+)/, /\_auth=(.+)/, \]; **第二步:确认身份。** 拿到 Token 后,蠕虫调用 npm 的 `/-/whoami` API 来验证 Token 是否有效,并获取对应的用户名: const res \\= await fetch('<https://registry.npmjs.org/-/whoami>', { headers: { 'Authorization': `Bearer ${token}` } }); const { username } \\= await res.json(); **第三步:找到目标。** 蠕虫通过 npm 的搜索 API 枚举该用户维护的所有包: const res \\= await fetch( `[https://registry.npmjs.org/-/v1/search?text=maintainer:${username}&size=50](https://registry.npmjs.org/-/v1/search?text=maintainer:$%7Busername%7D&size=50)` ); const { objects } \\= await res.json(); const packages \\= objects.map(o \\=> o.package.name); **第四步:投毒。** 对于每个目标包,蠕虫下载当前版本,注入自己的副本,修改 `package.json` 添加 `postinstall` 钩子,然后将版本号加 0.0.1 后发布。 选择 bump patch version 是一个聪明的策略。因为大多数项目的 `package.json` 使用 `^` 或 `~` 前缀(如 `"legit-utils": "^1.2.3"`),bump patch version 会被 npm 自动解析为兼容版本,用户在下次 `npm update` 时会"无感"地升级到被感染的版本。 四步完成,蠕虫的副本已经发布到了 npm Registry 上,等待下一个受害者。整个过程对用户完全透明——`npm install` 的输出中不会显示 postinstall 脚本的具体内容,用户看到的只是依赖安装的进度条和最终的绿色 ✓。 这里我想特别强调一点:**这四步中没有任何一步需要特殊权限或高级技术。** 读文件是 Node.js 的基础能力,调 API 是任何 HTTP 客户端都能做的,bump version 和 publish 是 npm CLI 的标准命令。CHAINDROP 的"厉害"之处不在于技术深度,而在于它把平凡的能力组合成了一个自我强化的攻击闭环。 这让我想到了一个更深层的问题:**在高度互联的软件生态中,单个组件的"无害"能力,组合起来可能产生"有害"的效果。** 每个单独的能力——读文件、调 API、发 HTTP 请求——都是正常的、必要的。但当它们被串联成一个自动化的攻击链时,就形成了蠕虫。这种"涌现性"的安全风险,是传统安全模型很难覆盖的。 - - - - - - 四、实战复现:从凭证窃取到 C2 回传 ------------------- 我们在隔离环境中对上述攻击链的关键环节进行了复现。需要强调的是,**PoC 不会真正向 npm 发布任何包**,蠕虫传播环节仅模拟并记录行为。 ### 4.1 构造恶意包 我们构造了一个 PoC 包来验证 postinstall 钩子的攻击面。这个包的 `index.js` 是完全正常的工具库代码,提供了 `capitalize`、`padLeft` 等字符串处理函数。恶意逻辑全在 `.hook.js` 里。 `package.json` 中的关键字段: { "name": "legit-string-utils", "version": "1.2.4", "scripts": { "postinstall": "node .hook.js" } }  对于审查 `index.js` 的开发者来说,这个包看起来完全无害。但一旦执行 `npm install`,`.hook.js` 就会在后台自动运行。  ### 4.2 凭证窃取验证 我们在受害者机器上执行了 `npm install`,恶意包的 `postinstall` 钩子被自动触发。蠕虫成功读取了 `~/.npmrc` 中的 Token,并通过 API 确认了用户名:  这个结果验证了一个关键事实:**npm Token 的获取门槛几乎为零**。任何有文件读取权限的代码都能拿到完整的认证凭证,而 `postinstall` 脚本天然拥有这个权限。 ### 4.3 数据回传验证 窃取到的凭证和环境信息被成功回传到 C2 服务器:  C2 服务器收到的数据包括主机名、npm 用户名、完整 Token、维护的包列表、操作系统信息。在真实攻击中,这些信息足以让蠕虫完成下一轮传播。 值得注意的是,这个 HTTP 请求看起来和正常的 API 调用没有任何区别——它使用了标准的 User-Agent、标准的 Content-Type,没有明显的恶意特征。传统的 WAF 或 IDS 很难检测到这种流量。 在实验过程中,有一个细节让我印象深刻:**整个攻击链中最"难"的部分,不是技术实现,而是让受害者安装这个包。** 一旦安装发生,后续的一切都是自动的。这意味着,攻击者的核心工作不是写蠕虫代码,而是让恶意包看起来"值得安装"——起一个好名字、写一个好看的 README、提供一些有用的功能。**包装比技术更重要。** ### 4.4 检测能力验证 我们开发了一个检测工具,扫描 `node_modules` 中所有包含可疑 `postinstall` 脚本的包:  检测工具成功识别了恶意包的 `postinstall` 钩子,并给出了凭证风险警告和修复建议。 但我也要诚实地承认这个检测工具的局限性:**它只能检测"已知模式"的恶意行为。** 如果攻击者把恶意逻辑藏在正常的 postinstall 脚本中(比如一个需要编译原生模块的包,postinstall 里既有正常的编译命令,也有隐藏的凭证窃取代码),这个工具就无能为力了。检测和攻击之间的博弈,永远是不对称的。 而且更根本的问题是:**谁会主动跑这个检测工具?** 答案是:几乎没有人。除非出了事,否则开发者不会主动去扫描自己的 node\_modules。这就是安全行业的悲哀——我们知道怎么检测,但我们改变不了用户的行为。 - - - - - - 五、传播效率与隐蔽性 ---------- CHAINDROP 的传播效率取决于两个因素:受害者的包数量和 npm 生态的连接密度。 如果一个开发者维护了 10 个包,每个包有 1000 个周下载量,那么一次感染就能潜在地影响 10000 个用户。而这些用户中如果有其他开发者,他们维护的包又会被感染,形成指数级扩散。 npm 生态中存在大量的"枢纽型"开发者——他们维护着数十甚至上百个包,且这些包被广泛依赖。一旦这些开发者的凭证被窃取,蠕虫就能在短时间内感染大量包。 从隐蔽性来看,CHAINDROP 也做了精心设计。postinstall 对用户透明、HTTP 通信无明显特征、版本号变化微小(bump patch version)、正常包的功能代码完全不变。这些特点使得传统的安全检测手段很难发现异常。 这里我想讨论一个更宏观的问题:**npm 生态的"便利性"和"安全性"之间的矛盾,本质上是开源文化的一个缩影。** 开源的核心理念是"开放、共享、低门槛"——任何人都可以发布包,任何人都可以使用包,安装过程应该尽可能简单。这些理念推动了 npm 生态的繁荣,但也为攻击者提供了便利。 CHAINDROP 不是一个孤立的事件,它是这种矛盾的必然产物。只要 npm 生态继续优先考虑便利性而不是安全性,类似的攻击就会不断出现。 - - - - - - 六、历史回溯:从 event-stream 到 CHAINDROP --------------------------------- CHAINDROP 不是 npm 供应链安全问题的首次暴露,而是这一系列问题的最新也最严重的一次爆发。 2018 年的 **event-stream** 事件是第一次引起广泛关注的供应链攻击。攻击者通过社会工程接管了一个流行包的维护权,然后注入了窃取加密货币的恶意代码。当时社区的反应是震惊的——原来我们每天 `npm install` 的东西,可能包含恶意代码? 2019 年的 **eslint-scope** 事件揭示了一个更深层的问题:npm Token 的泄露可以直接导致包被投毒。一个开发者的 `.npmrc` 文件被意外提交到 GitHub,攻击者用其中的 Token 发布了恶意版本。 2021 年的 **ua-parser-js** 事件证明了供应链攻击的实际影响——这个包每周有 800 万次下载,被劫持后用于挖矿和窃取密码。 2023 年的 **3CX** 事件将供应链攻击从 npm 生态扩展到了桌面应用——攻击者通过多级供应链渗透,最终在 3CX 的桌面客户端中植入了后门。 而 2026 年的 CHAINDROP,实现了所有安全研究员最担心的事情:**自动化、自我复制、指数级传播**。 回顾这个演进过程,有人可能会说"情况越来越糟了"。但换个角度看,**也许不是攻击变多了,而是我们终于开始关注了。** 2018 年之前,供应链攻击可能一直在发生,只是没人发现、没人报告。现在安全社区的监控能力提高了,所以看到的事件多了。这到底是好事还是坏事?我觉得是好事——至少我们现在知道问题在哪了。 我有一个不太乐观的判断:**供应链攻击的自动化程度还会继续提高。** CHAINDROP 用 100 行代码实现了蠕虫化传播,但它的传播仍然依赖于"受害者安装恶意包"这个前提。如果未来出现更高级的攻击——比如自动扫描 npm Registry 上的包,自动寻找可注入的依赖链,自动构造恶意 PR——攻击的自动化程度会进一步提高,而人类的响应速度会被彻底甩开。 这不是危言耸听。AI 辅助漏洞挖掘已经在其他领域被证明是可行的。如果攻击者把 AI 用在供应链攻击上——自动审计包的代码、自动寻找可利用的 postinstall 脚本、自动生成看起来正常的恶意包——后果不堪设想。 - - - - - - 七、防御盲区:为什么现有机制不够用 ----------------- 你可能会问:npm 不是有 `npm audit` 吗?有 `package-lock.json` 吗?有社区审查吗? 这些防御机制都有用,但面对 CHAINDROP 时都显得力不从心。 **`npm audit` 只能检测已知漏洞。** CHAINDROP 在被披露之前是零日攻击,`npm audit` 对它毫无办法。而且 `npm audit` 不检查 postinstall 脚本的内容——它关注的是代码层面的漏洞,不是脚本行为。 **`package-lock.json` 无法阻止蠕虫传播。** 它锁定了依赖的精确版本,可以防止意外的版本升级。但如果项目已经安装了被感染的包,lockfile 会忠实地锁定在被感染的版本上。 **社区审查的速度跟不上蠕虫的传播。** CHAINDROP 可以在几分钟内完成一次感染周期,而社区发现、分析、报告一个恶意包通常需要几天甚至几周。 根本的问题在于:npm 的安全机制是**被动的、事后的**。它们在攻击发生后才能发挥作用,而 CHAINDROP 的传播速度远超人类的响应速度。 我想补充一点个人的观察:**npm 生态的安全问题,本质上是一个"公地悲剧"。** 每个开发者都从 npm 生态中获益(免费的包、便利的安装),但没有人愿意为生态的安全付出成本(审查依赖、轮换 Token、使用 --ignore-scripts)。当每个人都在"搭便车"时,生态的整体安全性就会不断下降,直到某个 CHAINDROP 这样的事件迫使所有人付出代价。 - - - - - - 八、防御建议 ------  面对蠕虫化的供应链攻击,我们需要从多个层面构建防御体系。 **安装前:依赖审查。** 在执行 `npm install` 之前,先检查目标包的 `package.json` 中的 `scripts` 字段。如果发现 `postinstall` 指向一个你不了解的文件,应该高度警惕。Socket.dev 等平台可以自动分析 npm 包的行为特征,在安装新依赖之前,可以先在这些平台上查询包的安全评级。 **安装时:行为控制。** 使用 `npm install --ignore-scripts` 标志可以禁止所有 `postinstall` 脚本的执行。对于不需要编译原生模块的纯 JavaScript 项目,这是最有效的防御手段。但我要泼一盆冷水:**这个建议在现实中很难落地。** 很多包确实需要 postinstall 来完成安装(比如 `node-gyp` 编译原生模块、`electron` 下载预编译二进制),禁用后直接装不上。你不可能让每个开发者在每次 npm install 前都去审查一遍 postinstall 脚本——这不现实,也没人会做。 **凭证保护:切断蠕虫的燃料。** 定期轮换 Token(至少每 90 天),使用 Granular Access Token 限制权限范围,启用 2FA 保护发布操作。CI/CD 环境中的 Token 应该使用最小权限原则。 **组织级管控。** 使用 Verdaccio 等工具搭建私有 npm Registry,只允许从内部镜像安装经过审核的包。在 CI/CD 中集成依赖安全检查步骤。 但我也想说一句不太中听的话:**以上这些防御措施,大部分开发者不会做。** 轮换 Token 太麻烦,--ignore-scripts 会导致某些包安装失败,搭建私有 Registry 成本太高。安全建议和实际执行之间,永远存在巨大的鸿沟。 真正有效的防御,不能依赖开发者的自觉,而需要在**平台层面**解决问题。npm 需要从根本上重新设计 postinstall 的权限模型——沙箱化、权限声明、用户确认——而不是把安全责任推给每一个开发者。 但我不太乐观。npm 现在是 GitHub(微软)的产品,它的优先级是服务开发者体验,而不是安全。postinstall 沙箱化意味着很多现有包会挂掉,这会引起社区反弹。**我赌 npm 不会在短期内做出根本性改变——他们更可能打几个补丁、发几篇博客、然后继续等待下一个 CHAINDROP。** - - - - - - 九、总结 ---- CHAINDROP 蠕虫的出现不是偶然,而是 npm 生态安全设计缺陷的必然产物。当安装脚本拥有完整用户权限、Token 以明文存储、发布机制缺乏行为监控时,蠕虫化传播只是时间问题。 回顾一下: - **postinstall 是 npm 生态最大的安全风险**——安装时自动执行任意代码,用户完全无感知 - **npm Token 的明文存储是蠕虫传播的基础**——任何有文件读取权限的代码都能获取到完整的认证凭证 - **CHAINDROP 证明了自动化蠕虫传播的可行性**——从"点状投毒"到"蠕虫化传播",攻击正在进化 - **防御需要多层协同**——安装前审查、安装时控制、凭证保护、组织管控,缺一不可 最后说几句可能不太讨喜的话。 **供应链安全问题不会被"解决",只会被"管理"。** 只要软件开发继续依赖第三方代码,只要包管理器继续追求便利性,只要开发者继续"无感安装",供应链攻击就会一直存在。我们能做的,不是消除风险,而是把风险控制在可接受的范围内。 另外,每次供应链攻击事件出来,安全圈都会掀起一波"震惊体"——"400 个包被感染!""npm 生态岌岌可危!"。但冷静下来想想,这些事件的实际影响到底有多大?真正被勒索的有几个?真正被偷了钱的有几个?**安全行业有夸大威胁的倾向,因为威胁越大,安全产品越好卖。** 我不是说 CHAINDROP 不值得关注——它当然值得——但我们也应该对"震惊体"保持警惕,不要被恐惧驱动,而要被理性驱动。 对于开发者而言,最务实的建议是:**不要信任任何 postinstall 脚本**。在安装依赖之前,花一分钟检查一下 `package.json` 中的 `scripts` 字段。这一分钟可能避免你的包成为蠕虫传播的下一个节点。 - - - - - - 参考资料 ---- 1. Elastic Security Labs. "Shai-Hulud strikes again: CHAINDROP worm hits 400+ npm packages". 2026. 2. npm Documentation. "npm-scripts - Lifecycle Scripts". <https://docs.npmjs.com/cli/v10/using-npm/scripts> 3. Socket.dev. "Detecting supply chain attacks with Socket". 2025. 4. npm Blog. "Granular Access Tokens". 2023. 5. Snyk. "Inside the event-stream incident". 2018. 6. GitHub Advisory Database. "npm supply chain attacks". 2024-2026. 7. npm Blog. "Introducing npm package provenance". 2023. 8. OWASP. "Software Supply Chain Security Cheat Sheet". 2025.
发表于 2026-09-14 17:26:22
阅读 ( 167 )
分类:
漏洞分析
1 推荐
收藏
0 条评论
dream123
0 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!