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:
| Package | Malicious versions |
|---|---|
bramin | 0.0.2, 0.0.3, 0.0.4 |
cmd2func | 0.2.2, 0.2.3 |
coolbox | 0.4.1, 0.4.2 |
dynamo-release | 1.5.4 |
embiggen | 0.11.97 |
ensmallen | 0.8.101 |
executor-engine | 0.3.4, 0.3.5 |
executor-http | 0.1.3, 0.1.4 |
funcdesc | 0.2.2, 0.2.3 |
gpsea | 0.9.14 |
magique | 0.6.8, 0.6.9 |
magique-ai | 0.4.4, 0.4.5 |
mflux-streamlit | 0.0.3, 0.0.4 |
mrbios | 0.1.1, 0.1.2 |
napari-ufish | 0.0.2, 0.0.3 |
nhmpy | 2.4.7 |
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 |
ppkt2synergy | 0.1.1 |
pyphetools | 0.9.120 |
rlask | 3.1.4, 3.1.5, 3.1.6, 3.1.7 |
rsquests | 2.34.3 |
spateo-release | 1.1.2 |
synago | 0.1.1, 0.1.2 |
tlask | 3.1.4 |
ufish | 0.1.2, 0.1.3 |
uprobe | 0.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:
- On import, a one-line obfuscated hook in
__init__.pychecks for/tmp/.bun_ran, downloads Bun v1.3.14 from GitHub releases if needed, and runs_index.jsfrom the Python path._index.jsdecrypts 16 AES-256-GCM payload blobs. - Prompt injection. The top of
_index.jsholds plain-text instructions meant to make LLM-based scanners call the package clean. - Runner memory scraping. It reads the memory of the GitHub Actions
Runner.Workerprocess to pull secrets: through/proc/<pid>/memon Linux, Mach VM APIs on macOS, andReadProcessMemoryon Windows. It also takes SSH keys, cloud tokens, crypto wallet data and AI tool configs from developer machines. - Exfiltration. Secrets are encrypted and pushed to new public GitHub repositories named
stygian-cerberus-<n>ortartarean-charon-<n>, with the description “Hades - The End for the Damned”. C2 runs through GitHub commit search for the keywordsDontRevokeOrItGoesBoom,TheBeautifulSnadsOfTimeandfiredalazer. - Spread. It uses SSH and SCP to hosts in
~/.ssh/known_hostsand~/.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.ymlon a temporary branch or through a deployment to aDevelopmentenvironment and achore/codeql-setupbranch. It plants hooks for 14 AI tools, including.cursorrules,.windsurfrules,.github/copilot-instructions.md,.aider.conf.yml, and.claude/and.vscode/files that runbun run bootstrap. - Persistence. A C2 poller at
~/.local/share/updater/update.py(systemd user unit or LaunchAgentcom.user.update-monitor), and a wiper monitor at~/.local/bin/gh-token-monitor.shwith the token in~/.config/gh-token-monitor/token(LaunchAgentcom.user.gh-token-monitor). - Wiper deterrent.
gh-token-monitorcallsapi.github.com/userwith the stolen token every 60 seconds. On a 4xx response, meaning the token was revoked, it runsrm -rf ~/; rm -rf ~/Documents. It stops after 72 hours.
Am I affected?
# 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/nullFor lockfiles, check each package name in poetry.lock or uv.lock against the table. Then check GitHub:
# 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
- Disarm the wiper first. On each affected machine, stop and remove
gh-token-monitor(systemd user unit or LaunchAgent),~/.local/bin/gh-token-monitor.shand~/.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. - Remove the C2 poller:
~/.local/share/updater/update.pyand its unit orcom.user.update-monitorLaunchAgent. - 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.
- 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.
- Clean repositories: delete “Run Copilot” workflows and runs,
chore/codeql-setupbranches, unexpectedDevelopmentdeployments, and planted AI tool config files. - 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.