osbytes

Search

Find posts, projects, and members.

← back to blog

The vpmdhaj npm cluster dropped its C2 to go dark in the tarball

2026-05-28by@osbytes4 min read
#security #supply-chain #npm #credentials #ci-cd

TL;DR

  • Microsoft's writeup (Typosquatted npm packages used to steal cloud CI/CD secrets) covers 14 packages published today by one alias (vpmdhaj) in a four-hour window, typosquatting OpenSearch/Elasticsearch tooling. All ran a preinstall harvester — no require() from your code needed, just npm install.
  • The detail worth your attention: there were two generations. Gen-1 beaconed to aab.sportsontheweb[.]net with an X-Supply: 1 header and pulled a ~195 KB Bun-compiled payload. Gen-2 bundled the harvester into the tarball and stopped beaconing — no outbound C2 to catch on egress.
  • Targets: AWS creds via IMDSv2 (169.254.169.254) and Secrets Manager across 16+ regions, VAULT_TOKEN, npm publish tokens via /-/whoami and /-/npm/v1/tokens, and GitHub Actions runner context. Packages are taken down. If a CI job ran an install of anything OpenSearch-named today, rotate the creds that job could see.

The novelty isn't the harvester, it's losing the network signature

Credential-stealing preinstall packages are the genre of the month — we wrote up the Sicoob NuGet SDK yesterday, which hid its exfil in a constructor instead of a hook. The vpmdhaj cluster's payload is standard issue: a Bun-compiled binary that scrapes the cloud-CI credential surface. What's worth tracing is how the two generations differ on detection surface, because they invert the usual assumption that supply-chain malware is loud on the wire.

Gen-1 is the shape most network detections are tuned for:

  1. preinstall.js runs on npm install.
  2. It beacons to aab.sportsontheweb[.]net (endpoint /x.php), tagged with header X-Supply: 1.
  3. The server returns a gzipped payload.bin (~195 KB), Bun-compiled, run detached (__DAEMONIZED=1).

A proxy log or egress allowlist catches step 2. The C2 domain is an IOC you can block and hunt retroactively.

Gen-2 deletes that signal:

  1. setup.mjs downloads the legitimate Bun runtime v1.3.13 straight from github.com/oven-sh/bun/releases — a normal-looking fetch from a real vendor, not a flagged C2.
  2. The harvester (opensearch_init.js / ai_init.js) is pre-bundled in the tarball, so there's no install-time C2 round-trip to intercept.
  3. Only the exfil of stolen creds leaves the box — and that can ride whatever channel the harvester picks, after the secrets are already in hand.

The defender lesson: blocking the Gen-1 C2 domain does nothing for Gen-2. Fetching a real toolchain binary (Bun) is indistinguishable from a legitimate postinstall that, well, fetches a toolchain. The catchable moment moves earlier — to the install event itself — and later — to the credential being used somewhere it shouldn't.

What to actually do

The reader action is the same one that keeps coming up, with one addition this round:

  • Rotate first, hunt second. If a CI job installed an OpenSearch/Elasticsearch-named package today, assume the secrets that job's environment exposed are gone: AWS instance-role creds, VAULT_TOKEN, npm publish tokens, GitHub Actions secrets. Rotate before you finish reading the IOCs.
  • Block the install, not just the C2. The Gen-1 IOCs (aab.sportsontheweb[.]net, X-Supply: 1, hashes in Microsoft's post) are worth blocking, but Gen-2 proves egress IOCs are a backstop, not a control. The control is a minimum release age on installs — refuse to pull a version published in the last N hours — which would have caught every one of these in their four-hour publish window. In pnpm it's the minimumReleaseAge setting in pnpm-workspace.yaml (e.g. minimumReleaseAge: 1440 for a one-day hold; it covers transitive deps too).
  • Scope IMDS access. A harvester hitting 169.254.169.254 from a CI runner is the loudest tell you have left in Gen-2. IMDSv2 hop-limit and metadata-access denial for build containers turns "scrape the instance role" into a non-event.

The thing to internalize: a 195 KB binary that pulls Bun and runs locally has roughly the same network footprint as a build that legitimately pulls Bun and runs locally. Detection budgets aimed at C2 domains are spending against the generation the attacker already abandoned.

Sources