Shai-Hulud "Hades" Wave — PyPI Supply-Chain Worm
.pth Python startup hook pulls the Bun runtime and runs an obfuscated payload that harvests cloud/CI tokens, then re-publishes via stolen credentials. Cross-ecosystem (npm + PyPI). Defensive hunt & harden pack — no malicious code.Executive Summary
The "Hades" wave of the Shai-Hulud supply-chain worm trojanized widely-used scientific and developer-tooling Python packages on PyPI — beginning 2026-06-08 with six bioinformatics packages published in under 60 seconds, growing to 26+ packages / 37 malicious wheels (part of 473 artifacts tracked across npm + PyPI since 1 June). Each malicious package ships a *-setup.pth file, which Python auto-executes on interpreter startup (no explicit import required). The hook downloads the Bun JavaScript runtime (v1.3.13/1.3.14) from GitHub and runs an obfuscated _index.js payload (16 encrypted components) that works cross-runtime, independent of Node.js.
The payload aggressively scrapes secrets — AWS/GCP/Azure tokens, Kubernetes secrets, GitHub PATs and Actions tokens, PyPI/npm/RubyGems publishing credentials, SSH keys, Docker configs, .env files, shell histories, and AI-assistant configs — via process-memory reads (/proc/{pid}/mem on Linux, Mach APIs on macOS, ReadProcessMemory on Windows). Stolen publishing tokens let it self-propagate to more packages, and a gh-token-monitor daemon threatens destructive action if tokens are revoked — an anti-rotation extortion mechanism. Stolen data is gzip + AES-256-GCM + RSA-2048 encrypted and exfiltrated to attacker GitHub repos named stygian-cerberus-* / tartarean-charon-*. The payload even carries prompt-injection text to fool LLM-based analyzers and sends decoy traffic to Anthropic servers.
.pth write into site-packages (N1), python spawning bun (Q1), and the gh-token-monitor/update-monitor persistence artifacts (Q3/N2).Source Review & Web Hunter Notes
| Tier | Source | Key Finding | Carry |
|---|---|---|---|
| 1 · Vendor research | Orca Security — "Hades PyPI Supply Chain Attack" | Full chain: .pth hook → Bun runtime → _index.js; harvested secret types; persistence artifacts; exfil repo patterns; affected package list | yes |
| 1 · Vendor research | Socket — "Mini Shai-Hulud, Miasma & Hades Worms" | Cross-ecosystem worm; 473 artifacts since 1 Jun; bioinformatics + MCP-dev targeting | yes |
| 1 · News of record | BleepingComputer — "19 science-focused PyPI packages" | Initial wave scope, names, timing (6→19→26) | yes |
| 2 · Vendor | Endor Labs / SecurityWeek / Tenable | Stolen-token propagation; 100+ npm+PyPI packages; Mini Shai-Hulud CVE-2026-45321 | partial |
Decisions: dev/CI-CD-first — native hunts (package inventory, GitHub org audit, CI-runner secret access) are the primary surface; endpoint CQL covers the Falcon-visible behaviors (.pth writes, python→bun, persistence services/plists, process-memory reads). Atomic IOCs (package names/versions, repo patterns, persistence files) are well-published and copy-ready in §9 — but they rotate as the worm spreads, so behavior is the durable signal.
Hunt Brief & Attack Chain
Working hypothesis: A developer or CI runner installs/updates a trojanized package; on the next Python invocation the .pth hook fires, pulls Bun, runs _index.js, scrapes secrets from process memory + files, installs a persistence daemon, exfiltrates to an attacker GitHub repo, and reuses stolen publishing tokens to trojanize further packages.
| Step | Behavior | Telemetry | Hunt Angle |
|---|---|---|---|
| 1 · Install | pip/poetry/uv installs a trojanized package; ships *-setup.pth into site-packages | pkg manager exec; file write | N1, N3; Q5 |
| 2 · Startup hook fires | Python interpreter auto-runs the .pth import hook | ProcessRollup2 | Q1 |
| 3 · Runtime pull | Hook downloads Bun (v1.3.13/1.3.14) from GitHub, runs _index.js | NetworkConnectIP4/DnsRequest; proc exec | Q1 / Q2 |
| 4 · Secret harvest | Process-memory scrape (/proc/{pid}/mem, Mach, RPM) + .env/SSH/Docker/token files | eBPF / file read | Q4; N4 |
| 5 · Persistence | gh-token-monitor / update-monitor systemd service or LaunchAgent | service/plist write | Q3; N2 |
| 6 · Exfil + worm | Encrypted blob → attacker GitHub repo (stygian-cerberus-*); re-publish via stolen tokens | GitHub audit; network | Q6; N5 |
Affected surface & telemetry
| Surface | Required Telemetry | Priority | Gap Risk |
|---|---|---|---|
| Developer workstations (Linux/macOS/Windows) | Process exec + file writes (Falcon sensor) | Critical | Medium — .pth/text writes weak on some platforms |
| CI/CD runners | Runner process/network telemetry; pipeline logs | Critical | High — ephemeral runners often unmonitored |
| Package registries (PyPI/npm) | Dependency inventory / SCA | High | Low — SBOM/lockfile scan |
| GitHub org | Audit log; repo/token activity | High | Medium — needs audit-log ingestion |
| Process-memory credential theft | eBPF / Linux sensor | Medium | High — /proc/*/mem reads not always surfaced |
Consolidated IOC Table
| Type | Value | Conf | Action | Context |
|---|---|---|---|---|
| Package artifact | embiggen, ensmallen, gpsea, ppkt2synergy, pyphetools (+21 more) | high | detect | Trojanized PyPI packages — remove/pin away (§9) |
| File | *-setup.pth in site-packages | high | hunt | Auto-exec startup hook (install-time IOC) |
| Persistence | gh-token-monitor.service / .plist; update-monitor.service | high | detect | Anti-rotation extortion daemon |
| Repo pattern | stygian-cerberus-* · tartarean-charon-* | high | hunt | Attacker GitHub exfil repos |
| Lock file | /tmp/.bun_ran · /tmp/tmp.0144018410.lock | medium | hunt | Payload execution markers |
ATT&CK Mapping
| Tactic | Technique | Observed Behavior | Query / Control |
|---|---|---|---|
| Initial Access | T1195.002 — Compromise Software Supply Chain | Trojanized PyPI packages via stolen publishing tokens | N3; SCA (§10) |
| Execution | T1059.006 / T1059.007 — Python / JavaScript | .pth startup hook → Bun runs _index.js | Q1, Q2 |
| Persistence | T1543.002 / T1543.001 / T1543.004 — Systemd / Launch Agent | gh-token-monitor / update-monitor daemon | Q3; N2 |
| Credential Access | T1552.001 / .004 / .005 — Creds in files / private keys / cloud metadata | .env, SSH keys, Docker, cloud/CI tokens | Q4; N4 |
| Credential Access | T1003.008 / T1057 — Proc filesystem / memory scraping | /proc/{pid}/mem, Mach, ReadProcessMemory | Q4 |
| Defense Evasion | T1027 — Obfuscated payload + LLM prompt-injection + decoy traffic | 16 encrypted components; anti-analysis | behavioral |
| Exfiltration | T1567.001 — Exfil to code repository | Encrypted blob → attacker GitHub repo | Q6; N5 |
| Lateral / Impact | T1195.002 (worm) — re-publish via stolen tokens | Self-propagation across packages | N5; token rotation |
Native Audit-Log Hunts (dev / CI-CD — primary surface)
| Hunt | Source | Logic | Response |
|---|---|---|---|
| N1 · Affected-package inventory | SCA / SBOM / lockfiles (pip freeze, poetry.lock) | Any environment resolving an affected package@version (§9 list) | Remove/pin away; rebuild env; rotate creds |
| N2 · Persistence artifacts | Endpoint file inventory | Presence of gh-token-monitor.service/.plist, update-monitor.service, /tmp/.bun_ran | Remove; treat host as compromised |
N3 · .pth hook in site-packages | File audit / EDR | New *-setup.pth in any Python site-packages referencing import/exec | Quarantine package; rebuild env |
| N4 · CI secret access spike | CI/CD + cloud audit logs | Unusual read of CI secrets / cloud tokens during/after a package install step | Rotate; review pipeline |
| N5 · Attacker GitHub activity | GitHub org audit log | New repos matching stygian-cerberus-*/tartarean-charon-*; unexpected PAT/Actions-token use; new workflows | Revoke tokens; remove repos/workflows |
CrowdStrike LogScale CQL Hunt Queries
.pth and process-memory reads vary by platform/sensor — pair with the native hunts (§6). Field names validated against the Falcon event reference.Looks for: python/pip as the parent of bun, or fetching the Bun release. Accomplishes: catches the startup-hook → Bun stage — Python has no business launching Bun. FP: rare legit Bun-in-Python tooling — verify the parent and the GitHub URL.
// HUNT: Python spawning the Bun runtime (Shai-Hulud .pth -> Bun loader) // MITRE: T1059.006, T1059.007, T1195.002 | CONF: high FP: low-med COST: low // FP NOTES: uncommon legit Bun-from-Python tooling; verify the GitHub bun release URL #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | ParentBaseFileName=/^(python[0-9.]*|python|pip[0-9.]*|uv|poetry)$/i | (FileName=/^bun$/i or CommandLine=/(oven-sh\/bun|bun-v1\.3\.1[34]|\.bun\/bin\/bun)/i) | table([@timestamp, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine], limit=200)
Looks for: bun running _index.js or process activity tied to the payload lock files. Accomplishes: confirms the JS payload execution stage.
// HUNT: Bun executing the Shai-Hulud _index.js payload // MITRE: T1059.007, T1027 | CONF: med-high FP: low-med COST: low // TUNING: correlate with Q1 on same host/time; check for /tmp/.bun_ran lock #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | FileName=/^bun$/i | CommandLine=/_index\.js|\.bun_ran|tmp\.0144018410\.lock/i | table([@timestamp, ComputerName, UserName, ParentBaseFileName, CommandLine], limit=200)
Looks for: creation of the named systemd user service or macOS LaunchAgent. Accomplishes: high-fidelity persistence signal — the daemon names are campaign-specific.
// HUNT: Shai-Hulud persistence daemon (systemd user service / LaunchAgent) // MITRE: T1543.002, T1543.001/.004 | CONF: high FP: low COST: low // FP NOTES: names are campaign-specific; any hit warrants investigation #event_simpleName=/ELFFileWritten|NewExecutableWritten|PeFileWritten/ | TargetFileName=/(gh-token-monitor|update-monitor)\.(service|plist)$|com\.user\.gh-token-monitor\.plist/i | table([@timestamp, ComputerName, UserName, ContextBaseFileName, TargetFileName], limit=200)
Looks for: bun/node/python accessing /proc/<pid>/mem (Linux) for secret scraping. Accomplishes: surfaces the memory-harvest stage. FP: debuggers/profilers — exclude sanctioned tooling. Requires the Linux sensor with file-access visibility.
// HUNT: Runtime reading /proc/<pid>/mem for credential scraping (Linux) // MITRE: T1003.008, T1057 | CONF: medium FP: medium COST: medium // REQUIRES: Linux sensor file-access telemetry // TUNING: exclude debuggers/profilers #event_simpleName=/FileOpen|ProcessFileOpen|OsfmFileOpen/ | TargetFileName=/^\/proc\/[0-9]+\/mem$/ | ImageFileName=/\/(bun|node|python[0-9.]*)$/i | table([@timestamp, ComputerName, UserName, ImageFileName, TargetFileName], limit=200)
Looks for: a .pth file written into site-packages by a package-install process. Accomplishes: catches the auto-exec hook at install time. FP: legitimate .pth files exist — flag *-setup.pth and unusual ones; verify contents.
// HUNT: Suspicious .pth startup hook written into site-packages // MITRE: T1195.002, T1059.006 | CONF: high FP: low-med COST: low // TUNING: legit .pth exist; focus on *-setup.pth and writes by pip/uv during install #event_simpleName=/NewExecutableWritten|FileWritten/ | TargetFileName=/site-packages\/.*-setup\.pth$|site-packages\\.*-setup\.pth$/i | table([@timestamp, ComputerName, UserName, ContextBaseFileName, TargetFileName], limit=200)
Looks for: bun/node resolving api.github.com (repo creation / token use). Accomplishes: surfaces exfil-to-repo and worm re-publish. FP: legit dev tooling hits GitHub constantly — correlate with Q1/Q2 and a recent install.
// HUNT: Bun/Node contacting the GitHub API (exfil-to-repo / worm re-publish) // MITRE: T1567.001 | CONF: medium FP: medium COST: medium // TUNING: correlate to Q1/Q2 + a recent package install; baseline normal dev GitHub use #event_simpleName=DnsRequest | DomainName=/(^|\.)(api\.github\.com|uploads\.github\.com)$/i | ContextBaseFileName=/^(bun|node)$/i | table([@timestamp, ComputerName, ContextBaseFileName, DomainName], limit=200)
Detection Recommendations (SCA / GitHub / CI)
| Detection | Logic | Benign Exclusions | Deployment Path |
|---|---|---|---|
| Affected-package gate | Block/alert on any build resolving an affected package@version (§9) | None — hard-block the known-bad versions | SCA / dependency firewall / CI gate |
| Attacker-repo / token-anomaly | GitHub audit: new stygian-cerberus-*/tartarean-charon-* repos; PAT/Actions token use from new IP/UA; new workflows | Sanctioned automation tokens | GitHub Advanced Security / SIEM correlation |
| Endpoint persistence (Q3/N2) | gh-token-monitor / update-monitor service or plist created | None expected | Falcon Custom IOA (file write) / scheduled search |
Machine-Readable IOC Appendix
Grouped IOC Quick-Copy
One-click blocks for SCA gating, endpoint sweeps, and GitHub audit. Package versions rotate as the worm spreads — re-check against the latest Socket/Orca lists.
# Bioinformatics / graph-ML cluster ensmallen==0.8.101 mflux-streamlit==0.0.3,0.0.4 nhmpy==2.4.7 ppkt2synergy==0.1.1 embiggen==0.11.97 gpsea==0.9.14 pyphetools==0.9.120 # Developer-tooling cluster bramin==0.0.2-0.0.4 cmd2func==0.2.2-0.2.3 coolbox==0.4.1-0.4.2 dynamo-release==1.5.4 executor-engine==0.3.4-0.3.5 executor-http==0.1.3-0.1.4 funcdesc==0.2.2-0.2.3 magique==0.6.8-0.6.9 magique-ai==0.4.4-0.4.5 mrbios==0.1.1-0.1.2 napari-ufish==0.0.2-0.0.3 nucbox==0.1.2-0.1.3 okite==0.0.7-0.0.8 pantheon-agents==0.6.1-0.6.2 pantheon-toolsets==0.5.5-0.5.6 spateo-release==1.1.2 synago==0.1.1-0.1.2 ufish==0.1.2-0.1.3 uprobe==0.1.3-0.1.4 # 26 packages / 37 wheels — re-verify against latest Socket/Orca advisory
# Linux (systemd user services) ~/.config/systemd/user/update-monitor.service ~/.config/systemd/user/gh-token-monitor.service # macOS (LaunchAgent) ~/Library/LaunchAgents/com.user.gh-token-monitor.plist # Execution markers / lock files /tmp/.bun_ran /tmp/tmp.0144018410.lock # Startup hook <python>/site-packages/*-setup.pth # auto-exec import hook ~/.bun/bin/bun (v1.3.13 / v1.3.14) # downloaded JS runtime
# Attacker exfil GitHub repos (encrypted blob upload) stygian-cerberus-* tartarean-charon-* # Hunt GitHub org audit log for: new repos matching the above; # PAT / Actions-token use from new IP/User-Agent; new/modified workflows. # Bun runtime fetched from: github.com/oven-sh/bun/releases/download/bun-v1.3.13|14 # Evasion: decoy traffic to Anthropic AI endpoints (network noise — not an IOC to block)
# Treat any affected host/runner as credential-compromised: 1. Remove/pin away from affected package versions; rebuild the environment from clean 2. ROTATE: GitHub PAT + Actions tokens, AWS/GCP/Azure keys, SSH keys, Docker creds, PyPI/npm/RubyGems publishing tokens (the gh-token-monitor daemon punishes slow rotation) 3. Revoke + reissue OIDC/CI secrets; rebuild CI/CD runners from images 4. Audit GitHub org: remove attacker repos/workflows; review commits since first install 5. Related: Mini Shai-Hulud (CVE-2026-45321) npm variant — check npm deps too
Hardening — Tiered & Deployable
Supply-chain prevention is dependency governance + secret hygiene, not endpoint blocking. Assume any affected host leaked its secrets.
- Remove/pin away from affected packages; rebuild affected envs & runners — M1051 / M1016. Pin exact known-good versions; rebuild CI runners from images.
- Rotate every reachable credential — M1027 / M1015. GitHub PAT/Actions, cloud keys, SSH, Docker, registry publishing tokens. The
gh-token-monitordaemon punishes slow rotation — rotate anyway and remove the daemon. - Audit the GitHub org — M1047. Remove attacker repos/workflows; review commits/PRs since first install; check for added deploy keys/secrets.
- SCA / dependency-firewall gate in CI — M1016. Hard-block the affected versions at install time.
- Short-lived / OIDC CI credentials, least privilege — M1026 / M1015. Kill long-lived PATs; scope tokens narrowly.
- Disable arbitrary install-time code / use
--no-build-isolationcontrols & hash-pinned lockfiles — M1016. Prefer wheels with verified provenance; require lockfile hashes. - Egress control on build runners — M1037. Allow-list package mirrors + GitHub; alert on unexpected runtime downloads (Bun).
- Require SLSA build provenance / signed artifacts (Sigstore) — M1016. Verify provenance before promotion; treat unsigned/forged-provenance artifacts as untrusted.
- Internal package mirror / allow-list with review gate — M1016. New/updated deps reviewed before they reach builds.
- Secretless builds + workload identity — M1015. No standing secrets on runners for the worm to scrape.
Containment Runbook
| Phase | Actions | Owner | Evidence |
|---|---|---|---|
| Isolate | Network-contain affected dev hosts/runners; freeze affected CI pipelines; quarantine the affected package versions in the internal mirror | SOC / Platform | Host list; pipeline IDs; N1 inventory |
| Triage | Confirm .pth hook + Bun + persistence artifacts (N2/N3); enumerate which secrets were reachable on each host/runner | IR | Artifact paths; secret reachability map |
| Eradicate | Remove persistence daemons + lock files; delete attacker GitHub repos/workflows; rebuild hosts/runners from clean images | IR / Platform | Removal log; rebuild tickets |
| Recover | Rotate ALL reachable credentials (GitHub/cloud/SSH/Docker/registry); reissue OIDC/CI secrets; restore clean pinned dependencies | IR / IT | Rotation log; clean lockfiles |
| Harden | SCA gate + provenance verification + egress control on runners; promote Q3/N5 detections | Platform / Detection Eng | CI gate live; audit monitoring on |
Detection Coverage Map & Validation
| Technique | Behavior | CQL | Native | Coverage |
|---|---|---|---|---|
| T1195.002 | Trojanized package installed | Q5 | N1, N3 | Good via SCA |
| T1059.006/.007 | .pth hook → Bun → _index.js | Q1, Q2 | — | Good |
| T1543 | Persistence daemon | Q3 | N2 | Good |
| T1003.008 / T1552 | Credential/memory scrape | Q4 | N4 | Partial needs Linux sensor / CI audit |
| T1567.001 | Exfil to GitHub repo | Q6 | N5 | Partial GitHub audit primary |
| T1195.002 (worm) | Re-publish via stolen tokens | — | N5 | GAP registry/GitHub audit + token rotation |
Validation gates: (1) confirm process + file telemetry on dev/CI hosts (Linux/macOS sensors), and GitHub/registry audit-log ingestion; (2) baseline normal python→bun / GitHub-API usage; (3) lab-test: install a benign package shipping a harmless .pth to fire Q5; create a test systemd service to fire Q3; (4) promote Q3 + the affected-package SCA gate + GitHub repo-pattern alert; keep Q1/Q6 as scheduled hunts.
Hunt Summary Ticket
TITLE: Hunt — Shai-Hulud "Hades" Wave (PyPI Supply-Chain Worm)
SEVERITY: Critical (self-propagating credential theft; dev + CI/CD blast radius)
SCOPE: Developer workstations + CI/CD runners installing affected PyPI packages
HYPOTHESIS: Trojanized package ships a .pth startup hook -> pulls Bun -> runs _index.js ->
scrapes cloud/GitHub/CI secrets -> persists (gh-token-monitor) -> exfil to
attacker GitHub repo -> re-publishes via stolen tokens (worm).
QUERIES RUN: Q1 python->bun | Q2 bun _index.js | Q3 persistence daemon | Q4 /proc/mem scrape |
Q5 .pth in site-packages | Q6 bun->GitHub API
+ Native N1-N5 (SCA inventory, persistence sweep, .pth audit, CI secret access,
GitHub org audit)
DO FIRST: N1 (affected-package inventory) + N2 (persistence sweep); rotate creds on hits
FINDINGS: <pending analyst execution>
GAPS: /proc/mem + worm re-publish need Linux sensor + registry/GitHub audit
ACTIONS: Remove/pin packages; rotate ALL reachable creds; rebuild runners; SCA gate +
provenance; promote Q3 + repo-pattern alert
OWNER: HuntPack
VERSION: v0.3 - 2026-06-12
Changelog
table() calls now carry an explicit row limit: the default is 200 and truncation is silent, so a capped result was indistinguishable from a complete one. Atomic-IOC sweeps (filters over 5+ hashes or C2 IPs) use limit=max so a wide infection is never silently under-scoped; behavioural hunts use limit=200, where exceeding the cap indicates the query needs tuning. Where present, event names that do not exist in the Falcon data model were corrected (e.g. ServiceInstalled is a Sysmon concept, not a Falcon event; ElfFileWritten is ELFFileWritten) — such queries could never return a row. No detection logic, fields, or IOCs changed.References
| Tier | Source | Used For |
|---|---|---|
| 1 | Orca Security — Hades PyPI Supply Chain Attack | Full chain, secret scope, persistence artifacts, exfil repos, package list |
| 1 | Socket — Mini Shai-Hulud, Miasma & Hades Worms | Cross-ecosystem worm, artifact counts, targeting |
| 1 | BleepingComputer — 19 science-focused PyPI packages | Initial wave scope, package names, timing |
| 2 | Endor Labs — Hades Wave · Tenable — Mini Shai-Hulud (CVE-2026-45321) | Token-propagation, npm variant, FAQ |
HuntPack v0.3 · Shai-Hulud "Hades" · Generated 2026-06-12 · Defensive use only — no malicious code. Package versions and exfil infrastructure rotate as the worm spreads; keep SCA + GitHub/registry monitoring live and re-verify against the latest Socket/Orca advisories. Validate field names in your tenant before promoting any query.