CriticalSupply chainMonitoring

Hades campaign: ensmallen 0.8.101 and 28 more PyPI packages scraped CI runner memory and threatened to wipe home directories

Hijacked releases of ensmallen and other graph ML, bioinformatics and AI packages stole CI secrets on import. Kill gh-token-monitor before revoking tokens.

Published
Updated
Fixed in
The malicious releases are removed from PyPI. No clean replacement versions were named; use a release published before 8 June 2026
Exploitation
Yes. Malicious releases were published to PyPI with stolen credentials on 8 June 2026

What happened

On 8 June 2026, StepSecurity reported the Hades campaign: malicious releases of PyPI packages used in graph machine learning, computational biology and bioinformatics, plus some AI-agent and typosquat packages (StepSecurity). StepSecurity links it to the Miasma actor and calls it a Python evolution of earlier Miasma npm activity. The OSV record calls it the “Hades” wave of the Shai-Hulud campaign and says the releases were published with stolen credentials (OSV PYSEC-2026-232). How those credentials were stolen is not disclosed. See our Shai-Hulud advisory.

Affected versions, from StepSecurity:

PackageMalicious versions
bramin0.0.2, 0.0.3, 0.0.4
cmd2func0.2.2, 0.2.3
coolbox0.4.1, 0.4.2
dynamo-release1.5.4
embiggen0.11.97
ensmallen0.8.101
executor-engine0.3.4, 0.3.5
executor-http0.1.3, 0.1.4
funcdesc0.2.2, 0.2.3
gpsea0.9.14
magique0.6.8, 0.6.9
magique-ai0.4.4, 0.4.5
mflux-streamlit0.0.3, 0.0.4
mrbios0.1.1, 0.1.2
napari-ufish0.0.2, 0.0.3
nhmpy2.4.7
nucbox0.1.2, 0.1.3
okite0.0.7, 0.0.8
pantheon-agents0.6.1, 0.6.2
pantheon-toolsets0.5.5, 0.5.6
ppkt2synergy0.1.1
pyphetools0.9.120
rlask3.1.4, 3.1.5, 3.1.6, 3.1.7
rsquests2.34.3
spateo-release1.1.2
synago0.1.1, 0.1.2
tlask3.1.4
ufish0.1.2, 0.1.3
uprobe0.1.3, 0.1.4

rlask, tlask and rsquests look like typosquats of flask and requests. As of 2 November 2026, none of the listed versions is on PyPI, and the nhmpy, rlask, rsquests and tlask projects return “not found” (PyPI).

How the payload works

Details from StepSecurity:

  1. On import, a one-line obfuscated hook in __init__.py checks for /tmp/.bun_ran, downloads Bun v1.3.14 from GitHub releases if needed, and runs _index.js from the Python path. _index.js decrypts 16 AES-256-GCM payload blobs.
  2. Prompt injection. The top of _index.js holds plain-text instructions meant to make LLM-based scanners call the package clean.
  3. Runner memory scraping. It reads the memory of the GitHub Actions Runner.Worker process to pull secrets: through /proc/<pid>/mem on Linux, Mach VM APIs on macOS, and ReadProcessMemory on Windows. It also takes SSH keys, cloud tokens, crypto wallet data and AI tool configs from developer machines.
  4. Exfiltration. Secrets are encrypted and pushed to new public GitHub repositories named stygian-cerberus-<n> or tartarean-charon-<n>, with the description “Hades - The End for the Damned”. C2 runs through GitHub commit search for the keywords DontRevokeOrItGoesBoom, TheBeautifulSnadsOfTime and firedalazer.
  5. Spread. It uses SSH and SCP to hosts in ~/.ssh/known_hosts and ~/.ssh/config. It mints PyPI or npm publish tokens from GitHub Actions OIDC and forges Sigstore SLSA provenance for republished packages. It plants a “Run Copilot” workflow that dumps all organisation secrets into an artifact, through .github/workflows/codeql.yml on a temporary branch or through a deployment to a Development environment and a chore/codeql-setup branch. It plants hooks for 14 AI tools, including .cursorrules, .windsurfrules, .github/copilot-instructions.md, .aider.conf.yml, and .claude/ and .vscode/ files that run bun run bootstrap.
  6. Persistence. A C2 poller at ~/.local/share/updater/update.py (systemd user unit or LaunchAgent com.user.update-monitor), and a wiper monitor at ~/.local/bin/gh-token-monitor.sh with the token in ~/.config/gh-token-monitor/token (LaunchAgent com.user.gh-token-monitor).
  7. Wiper deterrent. gh-token-monitor calls api.github.com/user with the stolen token every 60 seconds. On a 4xx response, meaning the token was revoked, it runs rm -rf ~/; rm -rf ~/Documents. It stops after 72 hours.

Am I affected?

bash
# Installed versions of every Hades package
pip show bramin cmd2func coolbox dynamo-release embiggen ensmallen executor-engine executor-http \
  funcdesc gpsea magique magique-ai mflux-streamlit mrbios napari-ufish nhmpy nucbox okite \
  pantheon-agents pantheon-toolsets ppkt2synergy pyphetools rlask rsquests spateo-release \
  synago tlask ufish uprobe 2>/dev/null | grep -E '^(Name|Version)'

# Exact pins in requirements files
grep -rnE '^(ensmallen==0\.8\.101|embiggen==0\.11\.97|pyphetools==0\.9\.120|gpsea==0\.9\.14|dynamo-release==1\.5\.4|spateo-release==1\.1\.2|nhmpy==2\.4\.7|ppkt2synergy==0\.1\.1|rsquests==2\.34\.3|(rlask|tlask)==3\.1\.[4-7])' \
  --include='requirements*.txt' .

# The payload file and run-once markers
find / -path '*site-packages*' -name _index.js 2>/dev/null
ls -la /tmp/.bun_ran /tmp/tmp.0144018410.lock /var/tmp/.gh_update_state 2>/dev/null

# Persistence (Linux and macOS)
ls -la ~/.local/share/updater/update.py ~/.local/bin/gh-token-monitor.sh ~/.config/gh-token-monitor/ 2>/dev/null
systemctl --user list-units --all 2>/dev/null | grep -E 'update-monitor|gh-token-monitor'
ls ~/Library/LaunchAgents/com.user.update-monitor.plist ~/Library/LaunchAgents/com.user.gh-token-monitor.plist 2>/dev/null

For lockfiles, check each package name in poetry.lock or uv.lock against the table. Then check GitHub:

bash
# Exfiltration repositories created with your tokens
gh repo list YOUR_USER --limit 1000 --json name,description \
  --jq '.[] | select((.name | test("^(stygian-cerberus|tartarean-charon)-")) or ((.description // "") | test("Hades"))) | .name'

# Planted workflow and branches in a repository
gh run list -R YOUR_ORG/REPO --limit 200 --json name,headBranch,createdAt --jq '.[] | select(.name=="Run Copilot")'
git ls-remote --heads origin | grep -F 'chore/codeql-setup'

Check GitHub environments for an unexpected Development deployment, and review commits that add the AI config files listed above.

What to do now

  1. Disarm the wiper first. On each affected machine, stop and remove gh-token-monitor (systemd user unit or LaunchAgent), ~/.local/bin/gh-token-monitor.sh and ~/.config/gh-token-monitor/. StepSecurity says the monitor exists to discourage revocation; revoking the token while it runs triggers the wipe. It stops by itself after 72 hours.
  2. Remove the C2 poller: ~/.local/share/updater/update.py and its unit or com.user.update-monitor LaunchAgent.
  3. Rotate every secret the GitHub Actions runner held in memory for jobs that imported a listed version, plus SSH keys, cloud tokens, PyPI and npm tokens and crypto wallets on developer machines. Then revoke the stolen GitHub tokens.
  4. Check for republished packages. If you publish to PyPI or npm through GitHub Actions OIDC, review releases made since 8 June 2026. The worm forged SLSA provenance, so a valid attestation does not prove a release was yours.
  5. Clean repositories: delete “Run Copilot” workflows and runs, chore/codeql-setup branches, unexpected Development deployments, and planted AI tool config files.
  6. Pin to a release published before 8 June 2026 for each package and rebuild environments. No clean replacement versions were named in the sources.

Why this matters

The wiper changes the usual order of incident response. Revoking a stolen token is normally the first move. Here, revoking it while the monitor still runs deletes the home directory. Disarm the host, then revoke.

Sources