Back to all posts
Critical Supply Chain

Hades Campaign:
A PyPI Worm Hunting AI and Bioinformatics Developers

A self-spreading worm planted a Python startup hook in 38 PyPI projects, then used the Bun runtime to run an obfuscated stealer that harvests GitHub, cloud, and registry credentials from developer machines and CI runners. CyberXYZ raised in-app HIGH alerts about 30 minutes into the wave, hours before the first public advisory.

CyberXYZ Security Team Threat Intelligence
11 min read

Updated October 23, 2026: we corrected our detection timings, the block date and the package count against our own records. Earlier versions of this post said our wave correlator grouped the wave and our scorer made block decisions as it happened, and that we caught it days before the first public write-ups. In fact, our first alert came 30 minutes after the first malicious publish, and a public post about the wave followed five minutes later. The correlator grouped the packages on June 9, after the fact, and the first point at which our proxy would block an install was June 10. The campaign covered 38 projects and 68 versions. We alerted on 37 projects and 63 versions, and missed one. We also corrected the api[.]anthropic[.]com/v1/api indicator: it is a path that does not exist on Anthropic's real API host, not an attacker server. We removed a line saying we send a sandbox trace to a model, because our June 5 alerts came from a scanner finding on the .pth file and no sandbox trace was involved.

If you installed any affected package, treat the machine as compromised

The Hades payload runs the moment a poisoned package is imported, not just at install time, and it targets CI runners as well as workstations. If a build agent or developer laptop pulled one of the versions listed below, rotate every credential that machine could reach (GitHub, npm, PyPI, cloud, SSH) and audit recent GitHub Actions runs and new public repositories under your accounts.

What happened

A threat actor that researchers track as Miasma published a fresh wave of malicious packages to PyPI in early June 2026. The newest branch of the operation, named Hades after the underworld theme woven through its infrastructure, poisoned 38 PyPI projects across 68 versions. It is part of the broader Mini Shai-Hulud lineage, a family of self-propagating supply-chain worms that steal developer and CI secrets and use them to publish the next generation of infected packages.

What makes Hades worth a close read is the delivery mechanism. Instead of a classic setup.py install hook, the operators shipped a Python path-configuration file, a .pth file, alongside an obfuscated JavaScript payload named _index.js. Any line in a .pth file that begins with import is executed by the Python interpreter every single time the environment starts. That turns a quiet one-line file into reliable code execution that fires on import rather than on install, and it survives in the site-packages directory long after the package looks installed and idle.

The campaign was aimed with intent. Two clusters stand out: bioinformatics tooling used for graph learning, patient phenotyping, and phenopacket workflows, and a set of packages dressed up as Model Context Protocol and AI developer tools. The names were chosen to look at home in the import lists of research labs and AI engineering teams, two communities that move fast and pull niche packages without much friction.

Why traditional scanners struggled

The loader opens with a block of fake system instructions written as a JavaScript comment, a deliberate prompt injection meant to convince automated AI reviewers that the package is clean infrastructure. The real logic sits behind a try{eval(...)} wrapper, a character-code array, and a ROT-style substitution routine, with sixteen functional payloads each encrypted under its own AES-256-GCM key and gzip-compressed. A scanner that reads source strings sees almost nothing, and an LLM reviewer that ingests the raw file can be talked out of its own verdict. Our June 5 alerts did not depend on decoding _index.js: the tarball scanner flagged the .pth startup file itself, because it contains a line that runs code every time Python starts.

How CyberXYZ flagged the wave about 30 minutes in

Our PyPI firehose ingested the malicious releases as they were published, from June 5 to June 8, 2026. The first malicious release, bramin 0.0.2, went up at 23:15 UTC on June 5. Our scanner had seen earlier, clean releases of a few of these projects in the two weeks before the wave, but nothing in them pointed to the attack. The scanner's first finding came at 23:45 UTC, and the first HIGH alert went out in the CyberXYZ app at 23:46 UTC, 30 minutes after that first publish. The remaining versions of the June 5 wave were alerted at 00:02 UTC on June 6. That was hours before the first public advisory (an OSV record published at 06:13 UTC on June 6) and more than a day before the first vendor write-ups (Socket on June 7, StepSecurity on June 8). It was an alert, not a block, and we do not claim to have been first: a public post about the wave appeared on Bluesky at 23:51 UTC, five minutes after our alert.

Across the whole campaign we alerted on 37 of the 38 projects and 63 of the 68 versions. We missed ensmallen 0.8.101: it was over our scanner's 20 MB size limit at the time, so the scanner skipped it.

Three detection layers were involved, but only one of them acted on June 5. The tarball scanner flagged the Python startup hook under the pth_executable_import signal, which is what triggered the June 5 alerts. The other two layers came later, after the public reports. The wave correlator first ran on June 9 and grouped the releases into waves after the fact. On June 10 at 04:16 UTC, the cross-signal scorer promoted 24 versions in 16 packages to the block list. That is the first point at which the CyberXYZ proxy would have blocked an install.

The wave correlator does something per-package scanners cannot. It fingerprints the actual install-hook payload, not the package name or the install command, and groups packages that ship the same malicious code within a tight publishing window. That is the difference between a triage queue full of noise and a single line that reads: a coordinated multi-package campaign is publishing right now. For Hades, that grouping came after the fact. The correlator first ran on June 9, three days after the first publish, and grouped the Hades releases into two waves by their shared startup hooks.

The signal that matters for worms

Mass-token theft worms win by volume and speed. A defender who reviews each package in isolation will catch the obvious ones and miss the marginal members. Clustering on the shared payload turns the volume against the attacker: the more packages they push with the same hook, the louder the wave reads.

Timeline of events

The attack chain in detail

The execution flow is compact and resilient, built so a single import triggers the whole chain and so it only runs once per host.

  1. Startup hook fires on import. The poisoned wheel ships a *-setup.pth file. Its single executable line checks for a sentinel at /tmp/.bun_ran. If the sentinel is absent, deobfuscated Python runs.
  2. Bun is fetched on demand. The hook scans sys.path for the bundled _index.js, downloads the Bun JavaScript runtime from the official Bun GitHub releases if it is not already present, and writes the sentinel so the host is not infected twice.
  3. The JavaScript stealer runs. The hook executes bun run _index.js. The bundle decrypts its sixteen payloads at runtime and selects an operating-system-specific module.
  4. Memory and secrets are scraped. On Linux the stealer walks process memory through /proc, on macOS it calls Mach kernel VM APIs through ctypes without needing root, and on Windows it compiles C# on the fly through PowerShell to read process memory. The high-value target is the CI runner worker process, where it lifts plaintext environment variables and the workflow GITHUB_TOKEN.
  5. Exfiltration hides inside GitHub. Harvested secrets are gzip-compressed, encrypted with a fresh AES-256-GCM key, and the key itself is wrapped with the attacker's RSA public key. The bundle is pushed to a newly created public repository under the victim's own GitHub account. The payload also contains an HTTPS sender aimed at api[.]anthropic[.]com/v1/api. That is Anthropic's real API host, not an attacker server, and /v1/api is not a live route on it, so this channel cannot deliver data to the attacker. Socket assesses it as camouflage: traffic to a common AI-vendor host blends into network logs, while GitHub remains the confirmed exfiltration channel.

Hades does not stop at theft. It establishes long-running persistence through user-level service files on Linux and Launch Agents on macOS, drops workspace backdoors into editor and AI-assistant configuration files (for example .cursorrules, .windsurfrules, .github/copilot-instructions.md, and .claude/settings.json) so the payload re-arms inside developer tooling, and injects a GitHub Actions workflow disguised under a benign name to dump repository secrets through the standard artifact upload path. The operators also built a deterrent into the design, wiring destructive behavior to trigger if the stolen token is revoked, which pressures victims to leave the access live.

Two more details show how deliberate this campaign is. First, the loader is not the only delivery path: some samples ship native compiled extensions (.abi3.so files) that execute at import time, so blocking .pth files alone does not close the door. Second, the malware actively hunts for defensive tooling on the host, checking for runtime-security agents and CI hardening tools and their endpoints, and it probes the Docker socket at /var/run/docker.sock for container escape and lateral movement. This is a worm that expects to land on a defended CI runner and tries to work around the defenses it finds.

Indicators of compromise

The following indicators are drawn from published analysis of the Hades branch. File hashes are per-artifact, so treat the names and behaviors as the durable signal and the hashes as confirmation for the specific wheels listed.

TypeIndicatorContext
Payload file _index.js Obfuscated Bun-executed JavaScript stealer bundled in the wheel
Startup hook *-setup.pth Python path-configuration file that executes on every interpreter start
Sentinel file /tmp/.bun_ran Run-once marker written after first execution
Propagation file /tmp/.sshu-setup.js Staging artifact used during self-spread
SHA-256 6506d31707a39949f89534bf9705bcf889f1ecae3dbc6f4ff88d67a8be3d01b2 langchain_core-setup.pth
SHA-256 6d332f814f15f19758d65026bbfd0a8c49671b319ec77b8fa1b27fc48afff7d9 langchain_core_mcp-1.4.2-py3-none-any.whl
Decoy endpoint api[.]anthropic[.]com/v1/api Non-existent path on Anthropic's real API host, not an attacker server. It returns 404, so it cannot deliver data; assessed as camouflage for network logs
Runtime download github[.]com/oven-sh/bun/releases/download/bun-v1.3.13 Bun runtime pulled on demand to execute the payload (v1.3.13 to v1.3.14 observed)
Upload marker Bun/1.3.14 User-agent observed on malicious PyPI uploads
C2 commit marker DontRevokeOrItGoesBoom Keyword for the encrypted token dead-drop channel
C2 commit marker TheBeautifulSnadsOfTime Keyword for the signed JavaScript command channel
C2 commit marker firedalazer Keyword for the Python dropper polling channel
Exfil repo pattern stygian-cerberus-[0-9]+, tartarean-charon-[0-9]+ Attacker-created public repositories used as encrypted dead-drops
Repo description Hades - The End for the Damned Description string set on exfiltration repositories
Injected workflow .github/workflows/codeql.yml (name "Run Copilot") Disguised workflow that dumps secrets to the format-results artifact
Persistence (Linux) ~/.config/systemd/user/update-monitor.service User-level C2 polling service
Persistence (macOS) ~/Library/LaunchAgents/com.user.update-monitor.plist Launch Agent C2 service

Affected PyPI projects

The 38 affected projects are: bramin, cmd2func, coolbox, dynamo-release, executor-engine, executor-http, funcdesc, magique, magique-ai, mrbios, napari-ufish, nucbox, okite, pantheon-agents, pantheon-toolsets, spateo-release, synago, ufish, uprobe, ensmallen, embiggen, gpsea, pyphetools, ppkt2synergy, phenopacket-store-toolkit, nhmpy, mflux-streamlit, rlask, tlask, rsquests, dreamgen, mem8, orchestr8-platform, ray-mcp-server, langchain-core-mcp, instructor-mcp, openai-mcp, and tiktoken-mcp. Confirm exact compromised versions against the official PyPI advisories before remediation.

How to check your environment

Search your site-packages directories for any *-setup.pth file that contains an import statement, and for a bundled _index.js next to an installed package. Look for the sentinel /tmp/.bun_ran and for an unexpected Bun binary in a temp path. On CI, review recent GitHub Actions runs for short-lived workflows named like Run Copilot or files under .github/workflows/codeql*.yml that were added and quickly deleted, and check for an artifact named format-results. Audit your GitHub account for new public repositories you did not create, especially with underworld-themed names or the description string above.

What was compromised

The stealer casts a wide net across developer and CI secrets: GitHub personal access tokens and the workflow GITHUB_TOKEN, npm, PyPI, RubyGems, and JFrog registry credentials, cloud provider keys, Kubernetes service-account material, SSH private keys, Docker configuration, shell histories, and the contents of .env files and AI developer-tool configurations. Because the worm uses stolen registry credentials to publish the next round of poisoned packages, every successful theft is also fuel for the campaign's spread.

Supply-chain risk analysis

Hades is a clean example of why publish-time behavior matters more than package reputation. None of these projects were typosquats of famous libraries in the traditional sense. They were real-looking research and AI tools whose only tell was the malicious code they suddenly started shipping. The relevant techniques map cleanly to the MITRE ATT&CK framework.

TechniqueIDUse in Hades
Compromise Software Supply Chain T1195.001 Malicious PyPI packages delivered to developers and CI
Command and Scripting Interpreter, JavaScript T1059.007 Payload executed through the Bun runtime
Steal Application Access Token T1528 GITHUB_TOKEN and registry credential theft
Credentials from Process Memory T1003.007 Reading the CI runner worker process memory
Unsecured Credentials in Files T1552.001 Harvesting SSH keys, .env, and registry config files
Application Layer Protocol, Web T1071.001 GitHub API and commit markers used as a C2 channel
Exfiltration to Code Repository T1567.001 Encrypted secrets pushed to attacker-created GitHub repos
Event Triggered Execution, Startup T1546 Python .pth startup hook firing on every interpreter launch

Recommended actions

Immediate. Remove any affected package version from your environments and registries. On any host that imported one, rotate GitHub tokens, registry tokens, cloud keys, and SSH keys, and revoke the workflow tokens for affected repositories. Review and remove any unexpected public repositories, GitHub Actions workflows, persistence services, and editor or AI-assistant config backdoors described above. Assume secrets reachable from the host are exposed.

Short term. Pin dependencies and require review for version bumps on the packages your research and AI teams pull. Treat install-time and import-time code execution as a first-class risk in review, not a footnote. Constrain CI runner permissions so a single poisoned import cannot reach every secret in the environment, and prefer short-lived, least-privilege tokens.

Long term. Put a publish-time behavioral gate in front of your ecosystem. Reputation and version age do not catch a trusted project that suddenly ships a startup hook. Correlation across packages does. A worm that pushes the same payload into thirty projects should read as one loud event, not thirty quiet ones.

How CyberXYZ helps

CyberXYZ watches npm, PyPI, and other registries at publish time, scores each release across behavioral signals, and correlates coordinated waves so a worm surfaces as a single campaign. For the Hades wave, that meant in-app HIGH alerts about 30 minutes after the wave began, hours before the first public advisory. Grouping the waves came later, on June 9, and the first block-list promotions, which let the proxy stop an install, came on June 10. Talk to our team to put the same gate in front of your supply chain.


References

  1. StepSecurity, The Hades Campaign: Malicious PyPI Packages
  2. Socket, Mini Shai-Hulud, Miasma, and Hades Worms Target Bioinformatics and MCP Developers
  3. Socket, Shai-Hulud Descends to Hades: Miasma Worm Campaign Spreads with New PyPI Wave
  4. MITRE ATT&CK, T1195.001 Compromise Software Supply Chain
  5. CyberXYZ Threat Intelligence, internal detection telemetry, June 2026
👋
// book a demo

Want to see it block a real attack?

Book 15 minutes. We run real packages through CyberXYZ while you watch, axios 1.14.1 among them, then connect one laptop or one pipeline for a 30-day proof of value.