在 Trivy 遭遇入侵之后,StepSecurity 的 AI Package Analyst 在多个 npm scope 中标记出了可疑的新版本发布——从而揭露了 CanisterWorm,这是一个由 TeamPCP 威胁行为者部署的自我传播型 npm 蠕虫。该蠕虫是第二次 Trivy 入侵事件(v0.69.4)的直接延续:攻击者在 Trivy 的 CI/CD 工具链中植入了凭证收割器,从受影响的管道中窃取了 npm 令牌,然后利用这些令牌在他们所能触及的每个命名空间中发布带有后门的补丁版本——包括 @opengov scope(16+ 个包)。
在 Trivy 遭遇入侵之后,StepSecurity 的 AI Package Analyst 在多个 npm scope 中标记出了可疑的新版本发布——从而揭露了 CanisterWorm,这是一个由 TeamPCP 威胁行为者部署的自我传播型 npm 蠕虫。该蠕虫是第二次 Trivy 入侵事件(v0.69.4)的直接延续:攻击者在 Trivy 的 CI/CD 工具链中植入了凭证收割器,从受影响的管道中窃取了 npm 令牌,然后利用这些令牌在他们所能触及的每个命名空间中发布带有后门的补丁版本——包括 @opengov scope(16+ 个包)。
每个被入侵的版本都通过 postinstall 钩子安装一个持久化的 Python 后门,建立一个无需 root 权限的 systemd 用户服务来实现持久化,并轮询托管在互联网计算机区块链上的命令与控制端点——使其极难被清除。另一个蠕虫组件从受害者的机器上收割 npm 令牌,并自动将恶意软件重新发布到它所能触及的每个包,继续传播。通过 C2 分发的第二阶段有效载荷携带着具有破坏性的 Kubernetes 功能以及针对地缘政治目标受害者的文件系统清除逻辑。
我们如何检测到它
StepSecurity 的 AI Package Analyst 实时监控每一个 npm 发布,将新版本与每个包的完整发布历史进行对比,以识别行为异常。警报开始出现在多个 npm scope 中的包上——每个都带有相同的一致性高置信度信号
- 一个
postinstall脚本首次出现在该包的历史记录中。@opengov/form-builder的每个先前版本都不包含安装脚本。版本0.12.3添加了"postinstall": "node index.js"——这段代码在每次npm install时自动执行,在任何人工审查之前。 - 一个 base64 编码的有效载荷嵌入在
index.js中。 该文件包含一个硬编码的BASE64_PAYLOAD常量——整个 Python 后门编码为一个单一的 base64 字符串,以规避静态分析。标准扫描器如果不解码嵌入的有效载荷,会完全遗漏它。分析师对其进行了解码并完整分析,揭示了 C2 URL 和执行逻辑。 - 活跃的凭证收割。 解码后的有效载荷包含一个
findNpmTokens()函数,从~/.npmrc、项目级.npmrc、/etc/npmrc、环境变量和一个实时的npm config get查询中读取 npm 认证令牌——并将它们直接传递给传播脚本。
同样的指纹在多个 scope 中重复出现。这不是一次性的账户入侵——这是一次协调的蠕虫部署。
StepSecurity AI Package Analyst 提要显示跨受影响 npm scope 的被标记包
@opengov/form-builder@0.12.3 的 AI Package Analyst 详细报告——严重判定,安全评分 0,完整发现列表
背景故事:CanisterWorm 如何入侵
要理解如此多的 npm scope 为何被感染,你需要了解 TeamPCP 如何构建这个蠕虫——以及它从哪里获得了密钥。
与 Trivy 的关联
2026 年 3 月初,拥有 Aqua Security GitHub 组织访问权限的攻击者发布了 Trivy 发布版本 v0.69.4——这是一个带有硬编码连接的恶意二进制文件,连接到 typosquat 域名 scan.aquasecurtiy.org。他们还入侵了 trivy-action 和 setup-trivy GitHub Actions,植入了凭证收割有效载荷,通过 /proc/<pid>/mem 读取 CI/CD 环境变量和运行器进程内存。
Trivy 被广泛应用于大量的安全扫描管道中。任何在暴露窗口期间运行过被入侵的 action 或二进制文件的工作流,其环境中的每个密钥——包括 NPM_TOKEN 值——都会被窃取并使用攻击者的 RSA-4096 公钥加密。
这些被盗的 npm 令牌就是 CanisterWorm 的燃料。
蠕虫部署
手握 npm 发布凭证的攻击者并没有简单地入侵一两个高知名度包然后消失。他们部署了一个自我复制的蠕虫,旨在最大化在那些令牌所能触及的每个命名空间中的传播范围。
CanisterWorm 内部解析:工作原理
第一阶段——postinstall 钩子
入口点简单得令人麻痹。被入侵的包版本将一个 postinstall 脚本添加到 package.json:
"scripts": {
"postinstall": "node index.js"
}这在每次 npm install 时自动触发——在任何应用代码运行之前,在任何人工审查输出之前。受害者只需要安装这个包。无需点击任何内容,无需打开任何文件,无需授予任何权限。
第二阶段——建立持久化
index.js 有效载荷使用混淆的 Node.js 将 Python 后门写入磁盘。创建两个文件:
~/.local/share/pgmon/service.py——Python 植入物~/.config/systemd/user/pgmon.service——使用Restart=always配置的 systemd 用户服务
无需 root 权限。该服务在登录时自动启动,失败时重新启动,在重启后持续存活。在开发者经常运行 npm install 作为正常工作一部分的机器上,这个后门将完全不可见。
第三阶段——互联网计算机 Canister C2
一旦 systemd 服务运行,service.py 开始轮询托管在互联网计算机协议(ICP)区块链上的命令与控制端点:
https://tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io/植入物大约每 50 分钟检查一次,从 /tmp/pglog 下载第二阶段二进制文件,并在 /tmp/.pg_state 中跟踪已检索的内容。
为什么选择 ICP? 互联网计算机 canister 没有集中式服务器可以被扣押,没有域名注册商可以接收滥用投诉,没有托管提供商会对清除请求做出响应。传统的阻止和干扰方法不适用。攻击者故意选择这种基础设施,使 C2 通道对安全社区的响应具有弹性。
第四阶段——自我传播:蠕虫组件
这正是使 CanisterWorm 成为蠕虫而不仅仅是后门的原因。在 Python 服务开始轮询其 C2 之前,一个分离的后台进程——deploy.js——在受害者机器上开始工作:
- 凭证收割:扫描
~/.npmrc、环境变量(NPM_TOKEN、NPM_TOKENS)以及用于发布令牌的 npm 配置 - Scope 枚举:查询 npm 以发现每个被盗令牌可以发布到的所有包
- 版本递增和重新发布:递增每个发现包的补丁版本,注入 CanisterWorm 有效载荷,并使用
--tag latest重新发布 - 蠕虫传播:每个安装这些包的新开发者都成为新的受害者——并成为新的传播载体
这就是 @emilgroup 和 @opengov scope 被感染的方式。拥有这些 npm 命名空间访问权限的开发者或 CI/CD 管道运行了被入侵的 Trivy 工作流。他们的令牌被窃取。CanisterWorm 完成了其余的工作。
第五阶段——破坏性有效载荷
通过 ICP 分发的第二阶段有效载荷不是被动的。根据植入物对其环境检测到的内容,后果从凭证窃取到完整基础设施破坏不等。
| 环境 | 目标 | 操作 |
|---|---|---|
| Kubernetes 集群 | 伊朗 | 跨所有节点部署破坏性 DaemonSet——删除主机文件系统,强制重启 |
| Kubernetes 集群 | 其他 | 部署持久化后门 DaemonSet |
| 非 Kubernetes | 伊朗 | 执行 rm -rf / --no-preserve-root |
| 非 Kubernetes | 其他 | 静默退出——持久化后门保持活跃 |
对于 Kubernetes 目标,有效载荷部署一个特权 DaemonSet——host-provisioner-iran——带有 tolerations: [operator: Exists] 以保证调度到集群中的每个节点。对于目标窗口外的受害者,有效载荷静默退出,让持久化后门完全运行以供将来使用。
重要提示: 非目标受害者的静默退出并不意味着安全。systemd 服务和 ICP 轮询后门保持完全活跃。无论破坏性有效载荷是否触发,攻击者都保留访问权限。
入侵指标
网络
tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io——ICP canister C2 端点scan.aquasecurtiy.org——Trivy 阶段外泄域名(aquasecurity.org 的 typosquat)
主机痕迹
~/.local/share/pgmon/service.py——Python 植入物(存在即确认恶意软件已执行)~/.config/systemd/user/pgmon.service——systemd 持久化单元/tmp/pglog——第二阶段二进制文件投放位置(存在即确认 C2 已投递)/tmp/.pg_state——下载状态跟踪器
Kubernetes
- 命名空间
kube-system中的 DaemonSethost-provisioner-iran - 命名空间
kube-system中的 DaemonSethost-provisioner-std
您应该怎么做
如果您已安装 AI Package Analyst 提要中标记的任何包:
- 立即检查后门痕迹:
~/.local/share/pgmon/service.py和~/.config/systemd/user/pgmon.service——它们的存在确认恶意软件已执行 - 检查
/tmp/pglog——它的存在意味着第二阶段有效载荷已被下载并运行 - 轮换在受影响的机器上或运行过这些包的任何 CI/CD 环境中存在的所有 npm 令牌
- 轮换安装时可访问的所有其他密钥(AWS 凭证、Docker 令牌、GitHub PAT)——假设它们已被外泄
- 审查面向外网络日志,查找与
tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io的连接 - 将受影响的依赖项固定到最后一个确认的干净版本,直到维护者发布修复版本
StepSecurity 如何提供帮助
StepSecurity 通过三个支柱提供端到端的 npm 供应链安全:预防、检测和响应。以下是每个支柱如何在此攻击中提供帮助——以及它们如何保护您免受下一次攻击。(完整文档)
预防——在恶意包进入您的代码库之前阻止它们
- npm Package Cooldown Check——新发布的 npm 包在可配置的冷却窗口期间被临时阻止。当 PR 引入或更新到最近发布的版本时,检查会自动失败。由于大多数恶意包在 24 小时内被识别,这创建了一个关键的安全缓冲。在这种情况下,
react-native-country-select@0.3.91和react-native-international-phone-number@0.11.8在冷却期间会被阻止进入任何 PR。 - npm Package Compromised Updates Check——StepSecurity 维护一个已知的恶意和高风险 npm 包的实时数据库,持续更新——通常在官方 CVE 之前。如果 PR 尝试引入被入侵的包,检查会失败,合并被阻止。
- Harden-Runner Egress Network Restrictions——在工作流执行期间过滤面向外网络流量,阻止所有未声明的端点。DNS 和网络级别强制执行都可防止隐蔽数据外泄——此恶意软件中的 Solana RPC 轮询和 C2 有效载荷获取将在网络级别被阻止。
检测——跨 PR、仓库和开发机器的持续可见性
- Threat Intelligence + AI Package Analyst——持续监控 npm 注册表中的可疑发布。在这种情况下,两个包在发布后5 分钟内被标记,让团队有时间进行调查、确认恶意意图,并在包积累大量下载之前通知维护者。
- npm Package Search——在您组织的所有仓库的所有 PR 中搜索,查找特定包被引入的位置。当发现被入侵的包时,立即了解爆炸半径——哪些仓库、哪些 PR、哪些团队受到影响。这适用于跨 PR、默认分支和开发机器。
- Harden-Runner Network Baselines——自动记录每个作业和仓库的面向外网络流量,建立正常行为模式并标记异常。揭示恶意 postinstall 脚本是否执行了外泄尝试或联系了可疑域。
响应——调查事件并评估组织范围的影响
- Threat Center——关于被入侵的包、被劫持的维护者和新兴攻击活动的实时警报,直接传送到现有 SIEM 工作流。警报包括攻击摘要、技术分析、IOC、受影响版本和修复步骤。
- Coordinated Remediation——结合威胁情报、包搜索和网络基线,创建受影响仓库的优先级列表,并提供一致的指导,使跨数十或数百个仓库的协调修复同时进行。