
























Elastic Security Labs released initial triage and detection rules for the Axios supply-chain compromise. This is a detailed analysis of the RAT and payloads.
Elastic Security Labs identified a supply chain compromise of the axios npm package, one of the most depended-upon packages in the JavaScript ecosystem with approximately 100 million weekly downloads. The attacker compromised a maintainer account and published backdoored versions that delivered a cross-platform Remote Access Trojan to macOS, Windows, and Linux systems through a malicious postinstall hook.
node_modulesOn March 30, 2026, Elastic Security Labs detected a supply chain compromise targeting the axios npm package through automated supply-chain monitoring. The attacker gained control of the npm account belonging to jasonsaayman, one of the project's primary maintainers, and published two backdoored versions within a 39-minute window.
The axios package is one of the most widely depended-upon HTTP client libraries in the JavaScript ecosystem. At the time of discovery, both the latest and legacy dist-tags pointed to compromised versions, ensuring that the majority of fresh installations pulled a backdoored release.
The malicious versions introduced a single new dependency: plain-crypto-js, a purpose-built package whose postinstall hook silently downloaded and executed platform-specific stage-2 RAT implants from sfrclak[.]com:8000.
What makes this campaign notable beyond its blast radius is the stage-2 tooling. The attacker deployed three parallel implementations of the same RAT — one each for Windows, macOS, and Linux — all sharing an identical C2 protocol, command structure, and beacon behavior. This isn't three different tools; it's a single cross-platform implant framework with platform-native implementations.
Elastic Security Labs filed a GitHub Security Advisory to the axios repository on March 31, 2026 at 01:50 AM UTC to coordinate disclosure and ensure the maintainers and npm registry could act on the compromised versions.
As the community flagged the compromise on social media, Elastic Security Labs shared early findings publicly to help defenders respond in real time.
This post covers the full attack chain: from the npm-level supply chain compromise through the obfuscated dropper, to the architecture of the cross-platform RAT and the meaningful differences between its three variants.
The compromise is evident from the npm registry metadata. The maintainer email changed from jasonsaayman@gmail[.]com — present on all prior legitimate releases — to ifstap@proton[.]me on the malicious versions. The publishing method also changed:
| Version | Published By | Method | Provenance |
|---|---|---|---|
axios@1.14.0 (legitimate) | jasonsaayman@gmail[.]com | GitHub Actions OIDC | SLSA provenance attestations |
axios@1.14.1 (compromised) | ifstap@proton[.]me | Direct CLI publish | None |
axios@0.30.4 (compromised) | ifstap@proton[.]me | Direct CLI publish | None |
The shift from a trusted OIDC publisher flow with SLSA provenance to a direct CLI publish with a changed email is a clear indicator of unauthorized access.
axios@0.30.3 published legitimately by jasonsaayman@gmail[.]comaxios@1.14.0 published legitimately via GitHub Actions OIDCplain-crypto-js@4.2.0 published by nrwise (nrwise@proton.me) — clean decoy to build registry historyplain-crypto-js@4.2.1 published by nrwise — malicious version with postinstall backdooraxios@1.14.1 published by compromised account — tagged latestaxios@0.30.4 published by compromised account — tagged legacyaxios@1.14.1 — Malicious, tagged latest at time of discoveryaxios@0.30.4 — Malicious, tagged legacy at time of discoveryplain-crypto-js@4.2.0 — Clean decoy, published to build registry historyplain-crypto-js@4.2.1 — Malicious, payload delivery vehicle (postinstall backdoor)Safe versions: axios@1.14.0 (last legitimate 1.x release with SLSA provenance) and axios@0.30.3 (last legitimate 0.30.x release).
The attacker tagged both the latest and legacy channels, maximizing the blast radius across projects using either the current or legacy axios API.
The entire delivery chain hinges on npm's postinstall lifecycle hook. Installing either compromised axios version pulls plain-crypto-js@^4.2.1 as a dependency, which declares:
"scripts": {
"postinstall": "node setup.js"
}
This causes setup.js to execute automatically during npm install — no user interaction required.
The setup.js file uses a two-layer encoding scheme to conceal its behavior:
All critical strings, module names, URLs, shell commands are stored in an encoded array stq[] and decoded at runtime. The decoded contents reveal the operational infrastructure:
After decoding its string table, the dropper checks os.platform() and branches into one of three delivery routines. Each sends an HTTP POST to http://sfrclak[.]com:8000/6202033 with a platform-specific body — packages.npm.org/product0 (macOS), packages.npm.org/product1 (Windows), packages.npm.org/product2 (Linux) — allowing the C2 to serve the correct payload from a single endpoint. The packages.npm.org/ prefix is a deliberate attempt to make outbound traffic appear as benign npm registry communication in network logs:
| Platform | Delivery Method | Stage-2 Location | Disguise |
|---|---|---|---|
| macOS | AppleScript via osascript downloads binary with curl | /Library/Caches/com.apple.act.mond | Apple system daemon |
| Windows | VBScript downloads .ps1 via curl, executes via renamed PowerShell (%PROGRAMDATA%\wt.exe) | %TEMP%\6202033.ps1 (transient) | Windows Terminal |
| Linux | Direct curl download and python3 execution | /tmp/ld.py | None |
The dropper performs two cleanup actions:
Post-incident inspection of node_modules/plain-crypto-js/package.json reveals no trace of the postinstall trigger. The malicious setup.js is gone. Only the lockfile and npm audit logs retain evidence.
The three stage-2 payloads: PowerShell for Windows, compiled C++ for macOS, Python for Linux are not three different tools. They are three implementations of the same RAT specification, sharing an identical C2 protocol, command set, message format, and operational behavior. The consistency strongly indicates a single developer or tightly coordinated team working from a shared design document.
The following properties are identical across all three variants:
mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)FirstInfo, BaseInfo, CmdResultkill, peinject, runscript, rundirrsp_kill, rsp_peinject, rsp_runscript, rsp_rundirThe spoofed IE8/Windows XP user-agent string is particularly notable, it is anachronistic on all three platforms, and its presence on a macOS or Linux host is a strong detection indicator.
On startup, each variant:
After initialization, the implant enters the main loop. The first BaseInfo heartbeat includes a comprehensive system profile. The same categories of data are collected on all platforms, though the underlying APIs differ:
| Data Collected | Windows Source | macOS Source | Linux Source |
|---|---|---|---|
| Hostname | %COMPUTERNAME% env var | gethostname() | /proc/sys/kernel/hostname |
| Username | %USERNAME% env var | getuid() + getpwuid() | os.getlogin() |
| OS version | WMI / registry | sysctlbyname("kern.osproductversion") | platform.system() + platform.release() |
| Timezone | System timezone | localtime_r() | datetime.timezone |
| Boot time | System uptime | sysctl("kern.boottime") | /proc/uptime |
| Install date | Registry / WMI | stat("/") or sysctl | ctime of /var/log/installer or /var/log/dpkg.log |
| Hardware model | WMI | sysctlbyname("hw.model") | /sys/class/dmi/id/product_name |
| CPU type | WMI | sysctlbyname() | platform.machine() |
| Process list | Full PID, session, name, path | popen("ps") (up to 1000) | Full /proc enumeration (PID, PPID, user, cmdline) |
Subsequent heartbeats are lightweight, containing only a timestamp to confirm the implant is alive.
The C2 response is parsed as JSON, and the type field determines the action. All three variants implement the same four commands:
kill — Self-termination. Sends an rsp_kill acknowledgment and exits. The Windows variant's persistence mechanism (registry key + batch file) survives the kill command unless explicitly cleaned up; the macOS and Linux variants have no persistence of their own.
runscript — Script/command execution. The operator's primary interaction command. Accepts a Script field (code to execute) and a Param field (arguments). When Script is empty, Param is run directly as a command. The execution mechanism is platform-native:
| Platform | Execution Mechanism |
|---|---|
| Windows | PowerShell with -NoProfile -ep Bypass |
| macOS | AppleScript via /usr/bin/osascript |
| Linux | Shell via subprocess.run(shell=True) or Python via python3 -c |
peinject — Binary payload delivery. Despite the Windows-centric naming ("PE inject"), all three platforms implement this as a way to drop and execute binary payloads:
| Platform | Implementation |
|---|---|
| Windows | Reflective .NET assembly loading via [System.Reflection.Assembly]::Load() |
| macOS | Base64-decodes and drops a binary, executes with operator-supplied parameters. |
| Linux | Base64-decodes a binary to /tmp/.<random 6-char string> (hidden file), launches via subprocess.Popen(). |
The Windows implementation has in-memory execution with no file drop but without disabling AMSI which will certainly flag on the Assembly load. The macOS and Linux variants take the simpler approach of writing a binary to disk and executing it directly.
rundir — Directory enumeration. Accepts paths and returns detailed file listings (name, size, type, creation/modification timestamps, child count for directories). Allows the operator to interactively browse the filesystem.
| Capability | Windows (PowerShell) | macOS (C++) | Linux (Python) |
|---|---|---|---|
| Persistence | Registry Run key + hidden .bat | None | None |
| Script execution | PowerShell | AppleScript via osascript | Shell or Python inline |
| Binary injection | Reflective .NET load injecting into cmd.exe | Binary drop + execute | Binary drop to /tmp/ + execute |
| Anti-forensics | Hidden windows, temp file cleanup | Hidden temp .scpt | Hidden /tmp/.XXXXXX files |
The macOS Mach-O binary delivered by the plain-crypto-js postinstall hook exhibits significant overlap with WAVESHAPER, a C++ backdoor tracked by Mandiant and attributed to UNC1069, a DPRK-linked threat cluster.
This campaign demonstrates the continued attractiveness of the npm ecosystem as a supply chain attack vector. By compromising a single maintainer account on one of the JavaScript ecosystem's most depended-upon packages, the attacker gained a delivery mechanism with potential reach into millions of environments.
The toolkit's most reliable detection indicator is also its most curious design choice: the IE8/Windows XP user-agent string hardcoded identically across all three platform variants. While it provides a consistent protocol fingerprint for C2 server-side routing, it is trivially detectable on any modern network — and is an immediate anomaly on macOS and Linux hosts.
Elastic Security Labs will continue monitoring this activity cluster and will update this post with any additional findings.
Elastic uses the MITRE ATT&CK framework to document common tactics, techniques, and procedures that advanced persistent threats use against enterprise networks.
Tactics represent the why of a technique or sub-technique. It is the adversary’s tactical goal: the reason for performing an action.
Techniques represent how an adversary achieves a tactical goal by performing an action.
The following observables were discussed in this research.
| Observable | Type | Name | Reference |
|---|---|---|---|
617b67a8e1210e4fc87c92d1d1da45a2f311c08d26e89b12307cf583c900d101 | SHA-256 | 6202033.ps1 | Windows payload |
92ff08773995ebc8d55ec4b8e1a225d0d1e51efa4ef88b8849d0071230c9645a | SHA-256 | com.apple.act.mond | MacOS payload |
fcb81618bb15edfdedfb638b4c08a2af9cac9ecfa551af135a8402bf980375cf | SHA-256 | ld.py | Linux payload |
sfrclak[.]com | DOMAIN | C2 | |
142.11.206[.]73 | ipv4-addr | C2 |
The following were referenced throughout the above research:
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。