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.
Introduction
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.
Key takeaways
- A compromised npm maintainer account (jasonsaayman) was used to publish two malicious versions of the widely used Axios HTTP client — 1.14.1 (tagged latest) and 0.30.4 (tagged legacy) — meaning a default npm install axios resolved to a backdoored package
- The malicious JavaScript deploys platform-specific stage-2 implants for macOS, Windows, and Linux
- All three stage-2 payloads are implementations of the same RAT — identical C2 protocol, command set, beacon cadence, and spoofed user-agent, written in PowerShell (Windows), C++ (macOS), and Python (Linux)
- The dropper performs anti-forensic cleanup by deleting itself and swapping its package.json with a clean copy, erasing evidence of the postinstall trigger from
node_modules
Preamble
On 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.
Campaign overview
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.
Timeline
- 2026-02-18 17:19 UTC —
axios@0.30.3published legitimately byjasonsaayman@gmail[.]com - 2026-03-27 19:01 UTC —
axios@1.14.0published legitimately via GitHub Actions OIDC - 2026-03-30 05:57 UTC —
plain-crypto-js@4.2.0published bynrwise(nrwise@proton.me) — clean decoy to build registry history - 2026-03-30 23:59 UTC —
plain-crypto-js@4.2.1published bynrwise— malicious version withpostinstallbackdoor - 2026-03-31 00:21 UTC —
axios@1.14.1published by compromised account — taggedlatest - 2026-03-31 01:00 UTC —
axios@0.30.4published by compromised account — taggedlegacy
Affected packages
axios@1.14.1— Malicious, taggedlatestat time of discoveryaxios@0.30.4— Malicious, taggedlegacyat time of discoveryplain-crypto-js@4.2.0— Clean decoy, published to build registry historyplain-crypto-js@4.2.1— Malicious, payload delivery vehicle (postinstallbackdoor)
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.
Code analysis
Stage 1: The plain-crypto-js dropper
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:
- Layer 1: String reversal followed by Base64 decoding
- Layer 2: XOR cipher using the key OrDeR_7077 with a position-dependent index (7 * i² % 10)
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:
Platform-specific delivery
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 |
Anti-forensics
The dropper performs two cleanup actions:
- Self-deletion: setup.js removes itself via fs.unlink(__filename)
- Package manifest swap: A clean file named package.md (containing a benign version 4.2.0 configuration with no postinstall hook) is renamed to package.json, overwriting the malicious version
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.
Stage 2: Cross-platform RAT
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.
Shared architecture
The following properties are identical across all three variants:
- C2 transport: HTTP POST
- Body encoding: Base64-encoded JSON
- User-Agent:
mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0) - Beacon interval: 60 seconds
- Session UID: 16-character random alphanumeric string, generated per-execution
- Outbound message types:
FirstInfo,BaseInfo,CmdResult - Inbound command types:
kill,peinject,runscript,rundir - Response command types:
rsp_kill,rsp_peinject,rsp_runscript,rsp_rundir
The 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.
Initialization and reconnaissance
On startup, each variant:
- Generates a session UID — 16 random alphanumeric characters, included in every subsequent C2 message
- Detects OS and architecture — reports platform-specific identifiers (e.g., windows_x64, macOS, linux_x64)
- Enumerates initial directories of interest (user profile, documents, desktop, config directories)
- Sends a FirstInfo beacon containing the UID, OS identifier, and directory snapshot
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.
Command dispatch
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 summary
| 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 |
Attribution
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.
Conclusion
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.
MITRE ATT&CK
Elastic uses the MITRE ATT&CK framework to document common tactics, techniques, and procedures that advanced persistent threats use against enterprise networks.
Tactics
Tactics represent the why of a technique or sub-technique. It is the adversary’s tactical goal: the reason for performing an action.
Techniques
Techniques represent how an adversary achieves a tactical goal by performing an action.
- Supply Chain Compromise: Compromise Software Dependencies
- Command and Scripting Interpreter: JavaScript
- Command and Scripting Interpreter: PowerShell
- Command and Scripting Interpreter: AppleScript
- Command and Scripting Interpreter: Unix Shell
- Command and Scripting Interpreter: Python
- Boot or Logon Autostart Execution: Registry Run Keys
- Obfuscated Files or Information
- Masquerading
- Hidden Files and Directories
- Process Injection
- Indicator Removal: File Deletion
- System Information Discovery
- Process Discovery
- File and Directory Discovery
- Application Layer Protocol: Web Protocols
- Non-Standard Port
- Data Encoding: Standard Encoding
- Ingress Tool Transfer
Observations
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 |
References
The following were referenced throughout the above research:
