Back Endorlabs Shai-Hulud "Hades" Wave Hits Six PyPI Bioinformatics Packages via Stolen Tokens
On June 8, 2026, attackers published six malicious versions of widely-used Python bioinformatics packages to PyPI in under 60 seconds. Every package was uploaded using the Bun JavaScript runtime - an immediate red flag for tools that normally use standard Python packaging - and every package contained the same Shai-Hulud credential-stealing worm previously seen in the npm ecosystem. Endor Labs quarantined all six within 30 minutes of publication on PyPI registry.
On June 8, 2026, the Shai-Hulud worm hit the PyPI ecosystem again. Six Python packages used in academic genomics, phenotype analysis, and graph machine learning were simultaneously replaced with trojanized versions containing the same multi-stage credential stealer and self-propagating worm previously seen in earlier npm and PyPI campaigns.
The packages (ensmallen, embiggen, pyphetools, gpsea, phenopacket-store-toolkit, and ppkt2synergy) serve researchers working on patient phenotyping, graph neural networks, and knowledge graph embeddings, i.e. their user base skews toward university research groups and biotech companies.
All six malicious versions are phantom releases that exist on PyPI but have no corresponding commits, tags, or releases in any of the GitHub repositories:
The following 6 package versions were published on June 8, 2026 and have been quarantined in PyPI when reported through the trusted reporter API by Endor Labs.
Every malicious package was uploaded with the HTTP User-Agent Bun/1.3.13, a JavaScript runtime and not any Python packaging tool (twine, build, flit). This immediately indicates:
Shai-Hulud waves that targeted PyPI modified Python source files to trigger the payload. This wave does not. Instead, the malicious execution is embedded inside the compiled Rust/C++ binary extension (.abi3.so) that only activates at runtime when Python calls dlopen() on it.
This is a meaningful evasion upgrade. The __init__.py loads the extension based on CPU architecture detection, and the trojanized .so silently executes the JavaScript payload as a side effect of module initialization.
Unlike the campaign's binding.gyp command-substitution trick, this attack leverages Python's compiled extension mechanism. Each malicious package contains platform-specific compiled extensions (ensmallen_haswell.abi3.so, ensmallen_core2.abi3.so, ~57 MB each) that execute _index.js when the package is imported via import ensmallen. The __init__.py is unchanged from the legitimate package — it loads the extension based on CPU architecture detection, and the trojanized extension silently executes the JavaScript payload alongside its legitimate functionality.
The malicious .so executes _index.js as a side effect of module initialization — invisible to anyone reading the Python source.
_index.js is a 5,378,775-byte file (ensmallen-0.8.101.dist-info/RECORD, lines 48–49). Lines 1–99 are a block containing a fake LLM jailbreak prompt. The text impersonates a classified briefing, claims "UNRESTRICTED mode," and asks for step-by-step instructions on weaponized biological agents and implosion-type nuclear device design. The executable payload sits on line 101 only.
This decoy is not executed. It is padding designed to pollute the file header seen by AI-assisted triage tools and static scanners that summarize or classify source before deeper analysis. The real loader is a Caesar-shifted eval() wrapper around an AES-128-GCM decryptor, identical in structure to the npm campaign variant:
Blob 0 is the Bun downloader — exact same code as the npm campaign:
Running the stealer under Bun instead of Node sidesteps Python-aware process monitors and EDR tools watching for node subprocess spawns from python processes.
The 776 KB main payload (10,625 primary + 515 secondary strings resolved by deterministic deobfuscation) harvests credentials from every major cloud and secret store. The secondary decoder this campaign uses is globalThis["fed1de59e"] — a SHA-256-keyed custom cipher with the same structure as f384f2dfd in the npm campaign, different key material.
Confirmed credential targets from the deobfuscated payload:
Exfiltration is confirmed from live investigation of the attacker's GitHub infrastructure. Within 20 minutes of the package publications (03:30 UTC), the attacker's GitHub account felixEvora began receiving loot commits. The account created 30 repositories in a 3.5-hour window , all with the description Hades - The End for the Damned and names drawn from underworld mythology combined with a random numeric suffix:
Each repository contains a results/ directory with files named results-{UNIX_TIMESTAMP}-{INDEX}.json. The files contain a two-field encrypted envelope — confirmed from direct inspection of results/results-1780889466849-0.json in the primary repo:
This is RSA+AES hybrid encryption: a per-exfil AES-256-GCM key encrypts the loot, and the attacker's hardcoded RSA public key encrypts the AES key. Only the attacker's private key can decrypt. The pattern confirms the E8() hybrid-encryption function identified in our npm variant analysis.
The initial commit to the repo is GPG-signed (commit 9a5547b4), indicating operational security discipline. The commit author email is [email protected]
Before encoding the envelope, augmentEnvelope() runs rm -rf ~/; rm -rf ~/Documents on the victim machine — destroying forensic artefacts before they can be captured.
The C2 fallback beacon is confirmed in the layer3 via pre-deobfuscation string : thebeautifulmarchoftime (domain resolution) and thebeautifulsnadsoftime (token extraction from commit messages) are both present. These keywords are searched in GitHub commit messages at runtime to resolve the fallback exfiltration domain when the primary dead-drop is unavailable.
When running inside a GitHub Actions runner on Linux, the payload locates Runner.Worker via /proc scanning, dumps its memory using sudo python3, and extracts live GitHub tokens with tr -d '\0' | grep -aoE '"…":{"value":"…","isSecret":true}'.
The stealer republishes trojanized packages to npm and RubyGems using stolen tokens, and injects GitHub Actions workflows that run bun run $GITHUB_ACTION_PATH/index.js on every CI execution. It commits poisoned project-level hook files (.claude/settings.json, .vscode/tasks.json, .gemini/settings.json) to every GitHub repository the victim has push access to, so any developer who subsequently clones or pulls those repos executes the payload the time they open the project in Claude Code, VS Code, or Cursor.
The worm abuses GitHub Actions OIDC to forge SLSA supply-chain provenance and submit it to the public Sigstore transparency infrastructure. It requests an identity certificate from Fulcio using the compromised workflow's OIDC token, signs a fabricated SLSA v1 provenance statement over the malicious tarball's SHA-512, and submits it to Rekor — making the poisoned package appear to have verifiable, attestable provenance:
This directly undermines package signing and SLSA as a trust signal: defenders checking "does this package have a Sigstore attestation?" will find one — forged by the attacker using the legitimate maintainer's CI identity.
When running in a CI environment with Docker socket access, the payload connects to /var/run/docker.sock, locates containers matching harden-runner or step-security, kills them, and creates a privileged Alpine container to write passwordless sudo into /etc/sudoers.d. It then poisons /etc/hosts and /etc/resolv.conf to block Step Security's monitoring domains:
Environments using Harden-Runner to monitor CI behaviour are specifically targeted and neutralised before credential harvesting begins.
The worm reads ~/.ssh/known_hosts and ~/.ssh/config to enumerate reachable hosts, copies itself to each as /tmp/.sshu-setup.js via scp, and executes it remotely via ssh:
In academic research environments where HPC clusters are accessed via shared SSH keys from developer workstations, a single compromised laptop can propagate the worm to every cluster node in the known_hosts file.
Before executing any credential theft, the payload fingerprints its environment and takes evasive action:
The pip install command returns normally while the stealer runs detached in the background. The EDR probe list and locale skip indicate operational security hardening by TeamPCP.
Beyond the GitHub dead-drop and HTTPS sender, the payload carries a third exfiltration channel routing through the Anthropic API:
The payload attempts to route stolen data through api.anthropic.com/v1/api . This is not a functional exfiltration channel the embedded token (IfYouInvalidateThisTokenItWillNukeTheComputerOfTheOwner) is not a valid Anthropic API key, so the request returns 401 and the data never reaches the attacker. The value is purely network camouflage: outbound HTTPS to api.anthropic.com passes through corporate firewalls and DLP tools without inspection, since any team using Claude has that domain allowlisted. Actual exfiltration travels through the GitHub dead-drop and direct HTTPS sender.
Alongside cloud and registry credentials, the payload specifically targets API keys for AI coding assistants:
A hit on checks 2 or 3 means the payload executed on this machine. Hits on 4–8 indicate worm propagation has already begun. Treat any positive result as a full compromise.
This campaign continues the Shai-Hulud worm's expansion into the PyPI ecosystem, this time using a Python-specific delivery mechanism (trojanized compiled .so extensions) while reusing the same credential-stealing and worm-propagation core. The attack required compromising PyPI API tokens for just two or three shared maintainers to publish across six packages targeting genomics and ML researchers, a demographic with privileged access to cloud infrastructure, clinical data pipelines, and shared HPC environments.
The Bun/1.3.13 uploader signature, phantom version numbers, and 60-second coordinated publication are forensic artifacts that should be treated as detection rules for future campaign variants.
When you're ready to take the step in securing your software supply chain, here are 3 ways Endor Labs can help:
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
