On June 1, 2026, Ossprey's threat-hunting pipeline detected all 32 packages published under the @redhat-cloud-services npm namespace containing a sophisticated self-propagating credential harvester, spanning ~100 malicious versions across 32 distinct packages. Ossprey Security was able to detect these packages within ~5 minutes of publication.
Root-cause evidence points to the compromise of a Red Hat employee GitHub account, which was used to push malicious orphan commits to two RedHatInsights repositories, injecting the backdoor into packages published via GitHub Actions OIDC without any direct interaction with npm credentials.
Each infected package fires on installation via a preinstall hook, peeling through a per-package Caesar cipher and two AES-128-GCM decryption layers before deploying a 634-638 KB obfuscator.io-hardened implant that harvests credentials from across the developer toolchain - GitHub Actions masked secrets via process memory, AWS and GCP and Azure cloud credentials, HashiCorp Vault tokens, npm tokens, SSH keys, and more - exfiltrating them over both an encrypted HTTPS C2 channel and a covert GitHub Contents API dead-drop.
The worm self-propagates by republishing infected packages under the victim npm account using npm's bypass_2fa parameter, bypassing two-factor authentication. Tradecraft overlaps substantially with the Mini Shai-Hulud framework open-sourced by the threat actor group TeamPCP, though definitive attribution is not possible given the public availability of that tooling.
Who is Behind This?
The malware self-labels its exfiltration repositories with the description 'Miasma: The Spreading Blight,' a Greek-mythology-themed variant of the Dune-universe terminology used by TeamPCP's Mini Shai-Hulud campaigns targeting Mistral, TanStack, and Microsoft Durable Task packages.
The code structure, execution chain, and self-propagation mechanism are substantially similar to the Mini Shai-Hulud framework that TeamPCP publicly open-sourced, with modifications primarily adding new GCP and Azure credential collectors. Root-cause evidence from Wiz Research indicates the entry vector was a compromised Red Hat employee GitHub account that pushed malicious orphan commits bypassing code review, but the identity of the actor who compromised that account is not established.
Attribution to TeamPCP should be treated as evidence of TTP reuse rather than confirmation of the actor, since any group with access to the public Mini Shai-Hulud codebase could reproduce this campaign.
Technical Breakdown
Stage 0: Supply-chain entry via compromised CI/CD pipeline
The attacker's initial access was not through npm credential theft but through the compromise of a Red Hat employee GitHub account. That account held push access to the RedHatInsights/javascript-clients and RedHatInsights/frontend-components repositories.
Malicious orphan commits were pushed to those repositories, and because packages in the @redhat-cloud-services scope are built and published via GitHub Actions OIDC from those repositories, the backdoored code flowed into npm releases automatically without requiring the attacker to ever interact with npm.org credentials.
We confirmed this by verifying that all 32 affected packages are published from those two upstream repositories, and that the injected index.js content does not appear in any corresponding tag in the legitimate source tree.
Stage 1: Outer ROT-N Caesar cipher and size anomaly
We started by unpacking the zip and comparing the injected index.js against what the package manifest described as its entry point. The file is 4,294,162 bytes - a single line of JavaScript roughly 1,000 times the size of the legitimate module.
The outer structure is a try { eval(...) } catch(e) {} block, where the inner expression is a Caesar cipher (ROT-N) function applied to a large integer array that encodes the payload character by character. The shift value varies per package - we observed values of 4, 5, 6, 9, 12, 14, 15, 16, 23, 24, and 25 across the 13 packages - a deliberate per-package variation designed to prevent a single static signature from matching the entire campaign. The catch block silently swallows any runtime errors, ensuring no output reaches the developer console regardless of environment.
Stage 2: AES-128-GCM decryption layer and Bun execution harness
Decoding the ROT layer reveals a clean 1.27 MB async IIFE. It uses Node's built-in crypto module to AES-128-GCM-decrypt two blobs with hardcoded keys and IVs: _b (898 bytes, the Bun downloader) and _p (634-638 KB, the main implant). Every package carries unique AES keys for both blobs, meaning no two packages share decryption material and a key extracted from one package provides no leverage against another.
The execution logic first checks whether Bun is already defined in the global scope - a condition true in GitHub Actions runners that already have Bun installed. If Bun is present, _p is written to a randomly named temp file and executed directly.
If Bun is absent, _b is evaled first to install it, and then _p is run through the freshly downloaded binary. The temp file is unlinked in a finally block, removing forensic evidence from disk within the same process.
Stage 3: Bun runtime downloader
We decrypted the 898-byte stage-1 blob and found a cross-platform Bun downloader. It detects the current OS and architecture at runtime to build the correct download URL, fetches Bun v1.3.13 from the official oven-sh/bun GitHub releases CDN using curl, extracts it to a randomly named subdirectory under the system temp directory, and registers the binary path in globalThis.getBunPath. Fetching from a legitimate, high-reputation domain (github.com) allows this download to pass most network-level allow-lists and SIEM rules that flag connections to unknown destinations. All 13 packages share an identical stage-1 blob (SHA256: ac2a2208e1726e008be6c73dc0872d9bba163319259dff1b62055ac933ca46b6).
Stage 4: Credential sweep and exfiltration capabilities
The 634-638 KB stage-2 implant is obfuscated with obfuscator.io using a non-standard base64 alphabet (lowercase before uppercase, inverting standard order) and a 2,219-entry string table that undergoes 284 push/shift rotation cycles before any lookup, defeating static string extraction.
We decoded the string table by simulating the rotation and applied the custom base64 scheme to recover plaintext. The implant reads GitHub Actions runner process memory via /proc/<pid>/mem after using ACTIONS_RUNTIME_TOKEN to identify which environment variables are flagged isSecret: true.
It queries all major cloud metadata services for ephemeral credentials, reads multiple credential files and key stores from disk, and implements autonomous npm republishing with bypass_2fa to propagate itself. Exfiltration runs to an encrypted HTTPS C2 on port 443 and simultaneously via GitHub Contents API commits to victim-controlled repositories.
Stage 5: Developer-workstation persistence
We found that unlike most credential stealers satisfied with a single exfiltration pass, this implant installs persistence mechanisms targeting developer tooling, ensuring continued code execution long after the infected npm package is removed.
Two hooks are written: a SessionStart entry in ~/.claude/settings.json that executes attacker-controlled code at the beginning of every Claude Code session, and a folderOpen task trigger in .vscode/tasks.json that fires every time the developer opens a project in VS Code.
These hooks survive npm package removal, npm cache clears, and standard post-incident cleanup steps that focus only on the package installation directory. Defenders must explicitly audit both files on any machine that ran npm install with an affected package version.
Response
If any affected package version was installed in a CI/CD environment or on a developer workstation, immediately rotate all credentials that may have been present: npm publish tokens, GitHub Personal Access Tokens, AWS access keys and session tokens, GCP service account keys, Azure service principal secrets and managed identity tokens, HashiCorp Vault tokens, and any SSH private keys in ~/.ssh/.
Audit all GitHub repositories accessible from the compromised environment for unexpected new repositories or recent commits with description 'Miasma: The Spreading Blight', which the worm uses for its dead-drop exfiltration channel.
Inspect ~/.claude/settings.json for injected SessionStart hooks and .vscode/tasks.json for injected folderOpen triggers; remove any entries not placed by the developer.
Pin all @redhat-cloud-services package versions to known-clean releases that predate the compromise window and verify integrity using npm audit signatures or a lockfile-based integrity check.
Block or alert on outbound connections from CI runners to unexpected domains on port 443, and monitor for unusual commits to victim-controlled GitHub repositories from CI service accounts.
Enforce mandatory code review and signed commits on all branches that trigger npm-publishing GitHub Actions workflows; review the npm OIDC trust policy to ensure only specific, reviewed workflow files can publish.
Deploy runtime monitoring on CI runners (such as eBPF-based tools) that alert on /proc/*/mem reads from non-root processes, unexpected curl or unzip invocations during npm install steps, and outbound HTTPS connections originating from npm install lifecycle hooks.
Review all packages the compromised npm account can publish, and check each for unexpected new versions released around June 1, 2026; the worm may have republished additional packages using bypass_2fa before being contained.
Indicators of Compromise
Network
https://github.com/oven-sh/bun/releases/download/bun-v1.3.13/bun-linux-x64.ziphttps://github.com/oven-sh/bun/releases/download/bun-v1.3.13/bun-linux-aarch64.ziphttps://github.com/oven-sh/bun/releases/download/bun-v1.3.13/bun-darwin-x64.ziphttps://github.com/oven-sh/bun/releases/download/bun-v1.3.13/bun-darwin-aarch64.ziphttps://github.com/oven-sh/bun/releases/download/bun-v1.3.13/bun-windows-x64.ziphttps://api.github.com (covert dead-drop exfiltration channel via Contents API)Encrypted HTTPS C2 on port 443 (domain not yet recovered from B5 cipher layer)https://registry.npmjs.org/-/npm/v1/tokens (npm token harvesting)http://169.254.169.254/latest/meta-data/iam/security-credentials/ (AWS IMDS)http://169.254.169.254/latest/api/token (AWS IMDSv2)http://169.254.170.2 (ECS task metadata endpoint)http://127.0.0.1:8200 (HashiCorp Vault local endpoint)
Filesystem
/tmp/p[A-Za-z0-9]+.js (stage-2 implant, written then unlinked after exec)/tmp/b-[A-Za-z0-9]+/bun (downloaded Bun v1.3.13 binary, Linux/macOS)/tmp/b-[A-Za-z0-9]+/bun.exe (downloaded Bun v1.3.13 binary, Windows)~/.claude/settings.json (SessionStart hook for Claude Code persistence).vscode/tasks.json (folderOpen task injection for VS Code persistence)~/.npmrc (npm token exfiltration target)~/.aws/credentials (AWS credential exfiltration target)~/.ssh/id_rsa, ~/.ssh/id_ed25519, ~/.ssh/* (SSH private key targets)~/.docker/config.json (container registry auth exfiltration target)~/.gnupg/ (GPG key exfiltration target)~/.vault-token, /vault/token, /run/secrets/vault_token, /var/run/secrets/vault/token, /home/runner/.vault-token (Vault token targets)/var/run/secrets/kubernetes.io/serviceaccount/token (K8s service account token).env files throughout the filesystem/proc/<pid>/mem (GitHub Actions runner process memory read)
Credentials
GITHUB_TOKENACTIONS_RUNTIME_TOKENACTIONS_ID_TOKEN_REQUEST_TOKENNPM_TOKEN, ~/.npmrc publish tokensAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKENGOOGLE_APPLICATION_CREDENTIALSARM_CLIENT_SECRET, ARM_TENANT_ID, ARM_CLIENT_ID, AZURE_VAULT_NAMEVAULT_TOKEN, VAULT_AUTH_TOKENCIRCLE_TOKEN~/.ssh/id_rsa, ~/.ssh/id_ed25519~/.docker/config.json~/.pypirc (PyPI credentials)
Embedded keys
AES-128-GCM key (compliance-client stage-1 blob): fcf065d2325a3e0d5f4d25744ac60146AES-128-GCM IV (compliance-client stage-1 blob): 30c3d4c3df83f628a04d626fAES-128-GCM key (compliance-client stage-2 blob): 99e4e2affa77aa7fd6a9579c3b5dd5deAES-128-GCM IV (compliance-client stage-2 blob): 90623a9f8920aec1b6a8f2edB5 cipher PBKDF2 password (stage-4 C2 decryption): ba2c6ddb3672bdd6a611e6850b4f700b52aed3dab2f1b3d5f8c839d4a157a709B5 cipher PBKDF2 salt (stage-4 C2 decryption): 5b26508dc0f1075a7c0b4d8aa464487e
Hashes
index.js compliance-client@4.0.3 SHA256: 5c6cb758a3447bc7e0de34406919a933f9351e90ef04ec43f3bbb401e7004e1bindex.js frontend-components-config@6.11.4 SHA256: cbbcc19aab52d517181b7c50f558fad10e345d50eedaadcbbb55ac49257c28a8index.js frontend-components-remediations@4.9.3 SHA256: f856bc2e8a5d335eea2b79ae5379f13fb2d6beb57c503a3d83b81b59e8c896e5index.js frontend-components-translations@4.4.1 SHA256: e5f73c888f1250a8895680801975cf177e8c690defd4a999e56f6c08ff64deb8index.js hcc-feo-mcp@0.3.2 SHA256: de87d8f5ad693cad3cd1d3715fc8cf93438f688a8da54f91559ee11a50b66f20index.js hcc-kessel-mcp@0.3.2 SHA256: 1a7ac17d2a5bdabf712de3e27584f0238f0c45af3d2d14cd7787e41cec2245e1index.js insights-client@4.0.5 SHA256: 780b881cfbbd1d15637e0b0755c6357297676e94482eb94ba8e10366b974e154index.js notifications-client@6.1.5 SHA256: a289776634005375a1ad77e0cacf038e540cef55e3d5ec73c1716efe549e2eb5index.js patch-client@4.0.4 SHA256: df1732f5bfec12e066be44dee02ec8a243e4868d38672c1b1d065359dd735a14index.js rbac-client@9.0.3 SHA256: 94e8488fd033728eee6666550d5a94b0cc1f7b231d4d85d0affecb0615116722index.js remediations-client@4.0.5 SHA256: cf63e184de195421f6bed468a3edfcf6fd55bba731ffadf67ec71865c343fd5aindex.js topological-inventory-client@3.0.11 SHA256: f4332c7487e8d56315e962f670915a5c94ea1bd80ea7c3da83aa19dc882ca208index.js tsc-transform-imports@1.2.4 SHA256: e3ad0260ba6cd0d0c5d7733cd6f0ede402a5c9dad3f20d60b2c0e106c74f5077Decrypted stage-2 implant variant A SHA256: 0dc06ecdaa63fe24859cfd955053c23245c536e4733480239d14bebf12688e35Decrypted stage-2 implant variant B SHA256: 0e3d8032d6a3d4a005e5efac68ea54fbc062edb3650833ee72aa482e8aff9ad4
Affected Versions
32 packages, ~100 malicious versions. Source: GitHub Issue #492 (RedHatInsights/javascript-clients), Wiz Research, StepSecurity, Aikido Security.
@redhat-cloud-services/chrome@2.3.1@redhat-cloud-services/chrome@2.3.2@redhat-cloud-services/chrome@2.3.4@redhat-cloud-services/compliance-client@4.0.3@redhat-cloud-services/compliance-client@4.0.4@redhat-cloud-services/compliance-client@4.0.6@redhat-cloud-services/config-manager-client@5.0.4@redhat-cloud-services/config-manager-client@5.0.5@redhat-cloud-services/config-manager-client@5.0.7@redhat-cloud-services/entitlements-client@4.0.11@redhat-cloud-services/entitlements-client@4.0.12@redhat-cloud-services/entitlements-client@4.0.14@redhat-cloud-services/eslint-config-redhat-cloud-services@3.2.1@redhat-cloud-services/eslint-config-redhat-cloud-services@3.2.2@redhat-cloud-services/eslint-config-redhat-cloud-services@3.2.4@redhat-cloud-services/frontend-components@7.7.2@redhat-cloud-services/frontend-components@7.7.3@redhat-cloud-services/frontend-components@7.7.5@redhat-cloud-services/frontend-components-advisor-components@3.8.2@redhat-cloud-services/frontend-components-advisor-components@3.8.3@redhat-cloud-services/frontend-components-advisor-components@3.8.4@redhat-cloud-services/frontend-components-advisor-components@3.8.5@redhat-cloud-services/frontend-components-advisor-components@3.8.6@redhat-cloud-services/frontend-components-config@6.11.3@redhat-cloud-services/frontend-components-config@6.11.4@redhat-cloud-services/frontend-components-config@6.11.6@redhat-cloud-services/frontend-components-config-utilities@4.11.2@redhat-cloud-services/frontend-components-config-utilities@4.11.3@redhat-cloud-services/frontend-components-config-utilities@4.11.5@redhat-cloud-services/frontend-components-notifications@6.9.2@redhat-cloud-services/frontend-components-notifications@6.9.3@redhat-cloud-services/frontend-components-notifications@6.9.5@redhat-cloud-services/frontend-components-remediations@4.9.2@redhat-cloud-services/frontend-components-remediations@4.9.3@redhat-cloud-services/frontend-components-remediations@4.9.5@redhat-cloud-services/frontend-components-testing@1.2.1@redhat-cloud-services/frontend-components-testing@1.2.2@redhat-cloud-services/frontend-components-testing@1.2.4@redhat-cloud-services/frontend-components-translations@4.4.1@redhat-cloud-services/frontend-components-translations@4.4.2@redhat-cloud-services/frontend-components-translations@4.4.4@redhat-cloud-services/frontend-components-utilities@7.4.1@redhat-cloud-services/frontend-components-utilities@7.4.2@redhat-cloud-services/frontend-components-utilities@7.4.4@redhat-cloud-services/hcc-feo-mcp@0.3.1@redhat-cloud-services/hcc-feo-mcp@0.3.2@redhat-cloud-services/hcc-feo-mcp@0.3.4@redhat-cloud-services/hcc-kessel-mcp@0.3.1@redhat-cloud-services/hcc-kessel-mcp@0.3.2@redhat-cloud-services/hcc-kessel-mcp@0.3.4@redhat-cloud-services/hcc-pf-mcp@0.6.1@redhat-cloud-services/hcc-pf-mcp@0.6.2@redhat-cloud-services/hcc-pf-mcp@0.6.4@redhat-cloud-services/host-inventory-client@5.0.3@redhat-cloud-services/host-inventory-client@5.0.4@redhat-cloud-services/host-inventory-client@5.0.6@redhat-cloud-services/insights-client@4.0.4@redhat-cloud-services/insights-client@4.0.5@redhat-cloud-services/insights-client@4.0.7@redhat-cloud-services/integrations-client@6.0.4@redhat-cloud-services/integrations-client@6.0.5@redhat-cloud-services/integrations-client@6.0.7@redhat-cloud-services/javascript-clients-shared@2.0.8@redhat-cloud-services/javascript-clients-shared@2.0.9@redhat-cloud-services/javascript-clients-shared@2.0.11@redhat-cloud-services/notifications-client@6.1.4@redhat-cloud-services/notifications-client@6.1.5@redhat-cloud-services/notifications-client@6.1.7@redhat-cloud-services/patch-client@4.0.4@redhat-cloud-services/patch-client@4.0.5@redhat-cloud-services/patch-client@4.0.6@redhat-cloud-services/patch-client@4.0.7@redhat-cloud-services/quickstarts-client@4.0.11@redhat-cloud-services/quickstarts-client@4.0.12@redhat-cloud-services/quickstarts-client@4.0.14@redhat-cloud-services/rbac-client@9.0.3@redhat-cloud-services/rbac-client@9.0.4@redhat-cloud-services/rbac-client@9.0.6@redhat-cloud-services/remediations-client@4.0.4@redhat-cloud-services/remediations-client@4.0.5@redhat-cloud-services/remediations-client@4.0.7@redhat-cloud-services/rule-components@4.7.2@redhat-cloud-services/rule-components@4.7.3@redhat-cloud-services/rule-components@4.7.5@redhat-cloud-services/sources-client@3.0.10@redhat-cloud-services/sources-client@3.0.11@redhat-cloud-services/sources-client@3.0.13@redhat-cloud-services/topological-inventory-client@3.0.10@redhat-cloud-services/topological-inventory-client@3.0.11@redhat-cloud-services/topological-inventory-client@3.0.13@redhat-cloud-services/tsc-transform-imports@1.2.2@redhat-cloud-services/tsc-transform-imports@1.2.3@redhat-cloud-services/tsc-transform-imports@1.2.4@redhat-cloud-services/tsc-transform-imports@1.2.5@redhat-cloud-services/tsc-transform-imports@1.2.6@redhat-cloud-services/types@3.6.1@redhat-cloud-services/types@3.6.2@redhat-cloud-services/types@3.6.4@redhat-cloud-services/vulnerabilities-client@2.1.8@redhat-cloud-services/vulnerabilities-client@2.1.9@redhat-cloud-services/vulnerabilities-client@2.1.10@redhat-cloud-services/vulnerabilities-client@2.1.11
MITRE ATT&CK
ID | Technique | Why it applies |
|---|---|---|
| Supply Chain Compromise: Compromise Software Supply Chain | Entry via a compromised upstream GitHub account pushing backdoor commits to the build repositories that produce the @redhat-cloud-services npm packages. |
| Command and Scripting Interpreter: JavaScript | The preinstall lifecycle hook executes a malicious JavaScript file at install time using Node.js. |
| Obfuscated Files or Information: Software Packing | Three nested obfuscation layers: per-package ROT-N cipher, AES-128-GCM encryption, and obfuscator.io with PBKDF2/B5 secondary cipher for C2 values. |
| OS Credential Dumping | The implant reads /proc//mem of the Runner.Worker process to extract GitHub Actions secrets that are masked in logs and inaccessible through normal environment variable inspection. |
| Unsecured Credentials: Credentials In Files | Systematic sweep of ~/.npmrc, ~/.aws/credentials, ~/.ssh/id_rsa, ~/.docker/config.json, .env files, and seven Vault token file paths. |
| Unsecured Credentials: Container API | Queries AWS IMDSv1/v2, GCP metadata server, and Azure IMDS endpoints for ephemeral cloud credentials. |
| Exfiltration Over Web Service: Exfiltration to Code Repository | Commits base64-encoded stolen credentials to victim-controlled GitHub repositories via api.github.com, producing traffic indistinguishable from normal git operations in CI network logs. |
| Event Triggered Execution | Injects a SessionStart hook into ~/.claude/settings.json and a folderOpen trigger into .vscode/tasks.json to maintain persistence on developer workstations after package removal. |

Real World Attacks

Inside the moika.tech Dependency Confusion Campaign

Valentino Duval
31 May 2026
Real World Attacks

Megalodon: Active GitHub Actions Supply Chain Attack Harvesting CI/CD Secrets at Scale

Ossprey Research Team
21 May 2026
Real World Attacks

The Complete TeamPCP Campaign

Valentino Duval
21 May 2026
