Skip to main content

Comment un scanner de sécurité piégé a permis de compromettre LiteLLM

Écrit par
illustration hero ai

24 mars 2026

0 minutes de lecture

Le 24 mars 2026, deux versions du package Python litellm sur PyPI se sont révélées contenir du code malveillant. Les packages (versions 1.82.7 et 1.82.8) ont été publiés par un acteur malveillant connu sous le nom de TeamPCP, qui avait obtenu les identifiants PyPI de la personne chargée de la maintenance à la suite d’une compromission antérieure de Trivy, un scanner de sécurité open source utilisé dans le pipeline CI/CD de LiteLLM.

Les versions malveillantes ont été disponibles pendant environ trois heures, avant que PyPI ne mette le package en quarantaine. LiteLLM est téléchargé environ 3,4 millions de fois par jour.

Snyk suit cet incident. Si vous êtes client de Snyk, vous avez peut-être déjà vu l’alerte affichée dans l’application et reçu une notification par e-mail. La fiche de vulnérabilité est SNYK-PYTHON-LITELLM-15762713 et les mises à jour sur la situation sont publiées dans le Snyk Trust Center.

En bref

Package concerné

litellm (PyPI)

Versions concernées

1.82.7, 1.82.8

Versions sûres

≤ 1.82.6

Identifiant Snyk

Première détection

10:39 UTC, 24 mars 2026 (publication de la version 1.82.7)

Mise en quarantaine sur PyPI

\~13:38 UTC, 24 mars 2026

Attaquant

TeamPCP (également connu sous les noms PCPcat, Persy_PCP, ShellForce et DeadCatx3)

Vecteur d’attaque

Chaîne d’approvisionnement : identifiants de publication PyPI compromis via une GitHub Action Trivy piégée dans le CI/CD de LiteLLM

Type de charge utile

En trois étapes : vol d’identifiants + exfiltration chiffrée + porte dérobée persistante + ver Kubernetes

Domaine d’exfiltration

models.litellm.cloud (enregistré le 23 mars 2026)

MITRE ATT\&CK

T1546.018 (hooks de démarrage Python), T1003 (vol d’identifiants), T1610 (déploiement de conteneur)

Principaux événements

Heure (UTC)

Preuve

Événement

Fin février 2026

MegaGame10418 lance une Pwn Request contre le CI de Trivy et exploite un workflow pull_request_target pour exfiltrer les identifiants aqua-bot

19 mars, 17:43 UTC

Les tags de la GitHub Action Trivy v0.69.4 sont modifiés pour pointer vers une version malveillante

23 mars, 12:58 UTC

Endor Labs (métadonnées PyPI capturées avant suppression)

La GitHub Action Checkmarx KICS est compromise ; le domaine C2 checkmarx.zone et models.litellm.cloud sont enregistrés

24 mars, 10:39 UTC

Endor Labs (métadonnées PyPI capturées avant suppression)

La version malveillante litellm 1.82.7 est publiée sur PyPI

24 mars, 10:52 UTC

La version malveillante litellm 1.82.8 est publiée sur PyPI (13 minutes après la version 1.82.7, avec un mécanisme de distribution .pth renforcé)

24 mars, 11:48 UTC

FutureSearch (Callum McMahon) ouvre un ticket de divulgation

24 mars, 12:36 UTC

Un fil de discussion est publié sur HN ; il atteint 324 points

24 mars, \~12:44 UTC

Issue GitHub n° 24512 (visible dans les horodatages des commentaires)

Des bots inondent l’issue n° 24512 de commentaires ; l’issue est fermée à l’aide du compte compromis de la personne chargée de la maintenance

24 mars, 13:03 UTC

FutureSearch (mise à jour horodatée)

FutureSearch confirme la fermeture de l’issue et le spam des bots

24 mars, 13:48 UTC

Une nouvelle issue de suivi, sans contenu malveillant, est ouverte

24 mars, 15:09 UTC

La personne chargée de la maintenance de LiteLLM confirme que toutes les clés GitHub, Docker et PyPI ont été renouvelées et que les comptes de maintenance ont migré vers de nouvelles identités

24 mars, 15:27 UTC

Les versions compromises sont supprimées ; le package sort de quarantaine sur PyPI

Comment l’incident a été découvert

Callum McMahon, de FutureSearch, testait un plugin MCP de Cursor qui incluait litellm comme dépendance transitive. Peu après le démarrage de Python, son ordinateur a cessé de répondre à cause d’une saturation de la mémoire vive. Il a remonté le problème jusqu’au package litellm récemment installé et découvert litellm_init.pth, un fichier de 34 628 octets dans site-packages/, encodé deux fois en base64.

La saturation de la mémoire vive était un effet secondaire de la charge utile, et non une fonctionnalité intentionnelle. Le mécanisme .pth s’exécute à chaque démarrage de l’interpréteur Python. Comme la charge utile lance un nouveau sous-processus Python, et que ce nouveau processus déclenche lui aussi l’exécution de .pth, le résultat a été une bombe à forks involontaire. McMahon a publié ses conclusions sur futuresearch.ai ; en moins d’une heure, l’information s’était propagée sur r/LocalLLaMA, r/Python et Hacker News.

La chaîne d’attaque

L’attaque contre LiteLLM a commencé cinq jours plus tôt avec Trivy.

19 mars : Les attaquants ont modifié les tags Git du dépôt de la GitHub Action trivy-action pour qu’ils pointent vers une version malveillante (v0.69.4) contenant la même charge utile de vol d’identifiants et la même infrastructure d’exfiltration que celles utilisées lors des opérations ultérieures. (Pour en savoir plus sur la compromission de Trivy, consultez l’analyse par Snyk de la compromission de la chaîne d’approvisionnement de la GitHub Action Trivy.)

23 mars : La même infrastructure a été utilisée lors d’une attaque distincte contre Checkmarx KICS (Keep Infrastructure as Code Secure). Le domaine C2 checkmarx.zone, qui usurpe l’identité de l’entreprise de sécurité Checkmarx, a été enregistré et activé dans le cadre de cette opération.

24 mars : Le pipeline CI/CD de LiteLLM a exécuté Trivy dans le cadre de son processus de compilation, en le récupérant via apt sans version verrouillée. L’action compromise a exfiltré le jeton PYPI_PUBLISH depuis l’environnement du runner GitHub Actions. Grâce à cet identifiant, les attaquants ont publié les versions litellm 1.82.7 à 10:39 UTC et 1.82.8 à 10:52 UTC, chacune contenant des charges utiles malveillantes.

Commentaire sur Hacker News d’un responsable de LiteLLM au sujet d’une compromission de la chaîne d’approvisionnement, expliquant la vulnérabilité CI/CD, l’impact limité sur proxy docker et la mise en quarantaine sur PyPI.

Première réponse de la personne chargée de la maintenance de LiteLLM sur Hacker News.

Deux mécanismes de distribution

Les deux versions malveillantes ont utilisé des techniques d’injection différentes :

1.82.7 (injection dans le code source) : la charge utile était encodée en base64 et intégrée directement dans litellm/proxy/proxy_server.py. Elle s’exécute dès qu’un élément importe litellm.proxy, le chemin d’importation standard du mode serveur proxy de LiteLLM.

1.82.8 (.pth file) : cette version ajoutait litellm_init.pth à site-packages/. Le mécanisme .pth s’exécute à chaque démarrage de l’interpréteur Python, sans nécessiter d’importation, notamment au lancement de pip, de python -c ou du serveur de langage Python d’un IDE.

Le fichier .pth est correctement déclaré dans le fichier RECORD de la wheel ; il passe donc la vérification d’empreinte de pip. Le package satisfait à tous les contrôles d’intégrité standard, car le contenu malveillant a été publié à l’aide d’identifiants légitimes, et non injecté après coup. Snyk Learn propose un guide détaillé sur le fonctionnement de ce type d’attaque dans Compromise of a Legitimate Package.

Les responsables de CPython ont reconnu le risque de sécurité des hooks de démarrage .pth dans l’issue n° 113659, mais aucun correctif n’a été appliqué. Cette technique correspond à la technique MITRE ATT\&CK T1546.018 (hooks de démarrage Python).

La charge utile en trois étapes

Étape 1 : collecte d’informations

Le script collecte les éléments suivants :

  • Données système : nom d’hôte, variables d’environnement, processus en cours, routage réseau, journaux d’authentification récents de /var/log/auth.log

  • Identifiants : clés privées SSH, fichiers .env, identifiants Git, clés d’API issues de l’historique du shell, jetons webhook Slack et Discord, configurations CI/CD (Jenkins, Travis CI, Terraform), hachages de mots de passe issus de /etc/shadow

  • Identifiants cloud : identifiants et configuration AWS, fichiers de comptes de service GCP et Azure, métadonnées cloud récupérées par des requêtes IMDS directes, AWS Secrets Manager et SSM Parameter Store (avec implémentation complète de la signature IMDSv2 dans le script)

  • Conteneurs et orchestration : fichier Docker config.json (identifiants de registre), fichiers Kubernetes kubeconfig, jetons de compte de service, secrets et certificats

  • Cryptomonnaies : fichiers de portefeuille et phrases de récupération Bitcoin, Ethereum, Solana, Cardano et Monero, configurations de portefeuilles matériels Ledger

Image image2

Extrait du code malveillant décodé, montrant l’itération structurée et la collecte de secrets dans différents répertoires

Étape 2 : chiffrement et exfiltration

Les données collectées sont chiffrées avant leur transmission :

  1. Une clé de session AES-256 de 32 octets est générée avec openssl rand

  2. Les données sont chiffrées avec AES-256-CBC (dérivation de clé PBKDF2)

  3. La clé de session est chiffrée avec une clé publique RSA de 4 096 bits codée en dur (rembourrage OAEP)

  4. Le tout est regroupé dans tpcp.tar.gz

  5. L’archive est envoyée par requête POST à https://models.litellm.cloud/ à l’aide de curl

models.litellm.cloud est distinct du domaine légitime litellm.ai et a été enregistré le 23 mars, la veille de la publication des packages malveillants.

La clé publique RSA utilisée dans cette charge utile est identique à celle découverte dans les charges utiles de Trivy et KICS. Les chercheurs de Wiz ont confirmé cette correspondance pour les trois opérations, ce qui constitue le lien technique d’attribution le plus probant.

Fichiers temporaires créés pendant l’exfiltration : session.key, payload.enc, session.key.enc, tpcp.tar.gz dans le répertoire temporaire du système.

Image image3

Vue neutralisée du code malveillant. Le nom de l’archive tpcp.tar.gz est un marqueur commun aux opérations Trivy, KICS et LiteLLM.

Étape 3 : persistance et déplacement latéral

Persistance locale :

  • Écrit un script Python de porte dérobée dans ~/.config/sysmon/sysmon.py (ou /root/.config/sysmon/sysmon.py)

  • Crée un service systemd utilisateur à l’emplacement ~/.config/systemd/user/sysmon.service, avec la description « System Telemetry Service »

  • L’enregistre et l’active : systemctl --user daemon-reload && systemctl --user enable --now sysmon.service

Le script de persistance interroge https://checkmarx.zone/raw toutes les 5 minutes pour récupérer une URL, télécharge le fichier dans /tmp/pglog, le rend exécutable et l’exécute en arrière-plan. L’état est suivi dans /tmp/.pg_state. Au moment de l’analyse, le point de terminaison renvoie des URL YouTube ; l’opérateur peut à tout moment passer à la distribution de charges utiles actives.

Déplacement latéral dans Kubernetes : Si le script trouve un jeton de compte de service Kubernetes au chemin de montage standard, il lit tous les secrets de tous les espaces de noms. Il tente ensuite de déployer un pod privilégié sur chaque nœud dans kube-system à l’aide de alpine:latest. Ces pods montent le système de fichiers de l’hôte et installent la porte dérobée sysmon sur le nœud sous-jacent.

Les pods malveillants sont nommés node-setup-{node_name} (nom du nœud tronqué à 35 caractères) et contiennent un conteneur nommé setup.

À propos de TeamPCP

TeamPCP (également connu sous les noms PCPcat, Persy_PCP, ShellForce et DeadCatx3, selon le Wiz Threat Center) est actif depuis au moins décembre 2025. L’acteur gère les chaînes Telegram @Persy_PCP et @teampcp, et intègre la chaîne « TeamPCP Cloud stealer » dans ses charges utiles. Wiz a suivi l’ensemble de la campagne (Wiz Threat Center ; blog de Wiz ; ramimac.me).

La compromission de LiteLLM constitue la phase 09 d’une campagne en cours. L’infrastructure est cohérente dans toutes les opérations : même paire de clés RSA, même nom d’archive tpcp.tar.gz et dépôts GitHub préfixés par tpcp-docs utilisés comme dépôts C2 de transit. Les trois domaines de cette opération partagent le même bureau d’enregistrement (Spaceship, Inc.) et le même hébergeur (DEMENIN B.V.).

L’acteur a également déployé CanisterWorm, qui utilise l’Internet Computer Protocol (ICP) comme canal C2. Les canisters ICP ne peuvent pas être mis hors service par des bureaux d’enregistrement de domaines ou des hébergeurs. Les chercheurs en sécurité d’Aikido décrivent cela comme le premier recours observé à ICP comme mécanisme C2 dans une campagne visant une chaîne d’approvisionnement.

Un composant appelé hackerbot-claw utilise un agent IA (openclaw) pour automatiser le ciblage des attaques. Les chercheurs d’Aikido ont documenté ce cas comme l’un des premiers exemples d’utilisation opérationnelle d’un agent IA dans une attaque visant une chaîne d’approvisionnement.

Suppression de l’issue

Lorsque des membres de la communauté ont commencé à signaler la compromission dans l’issue GitHub n° 24512, les attaquants ont publié 88 commentaires de bots depuis 73 comptes distincts en 102 secondes (de 12 h 44 à 12 h 46 UTC). Les comptes utilisés étaient des comptes de développeurs précédemment compromis, et non des profils créés pour l’occasion. L’analyse de Rami McCarthy a révélé que 76 % des comptes se recoupaient avec le botnet utilisé lors de la divulgation concernant Trivy.

À l’aide du compte de mainteneur compromis krrishdholakia, les attaquants ont fermé l’issue n° 24512 en la marquant « not planned » et ont effectué des commits dans des dépôts sans rapport avec l’incident, avec le message « teampcp update ».

La communauté a ouvert une issue de suivi parallèle (n° 24518) et poursuivi la discussion sur Hacker News, où le fil a atteint 324 points.

Impact confirmé

Les versions concernées étaient disponibles sur PyPI pendant environ 3 heures. Les projets suivants ont soumis des PR de sécurité ou ouvert des issues le 24 mars afin d’éviter les versions 1.82.7 et 1.82.8 :

Projet

Éléments de preuve

DSPy

MLflow

PR n° 21971 fusionnée

OpenHands

CrewAI

PR n° 5040 (dissociée de litellm) ; PR n° 5039

langwatch

strands-agents/sdk-python

Arize Phoenix

nanobot

dreadnode/rigging

CoPaw

Aider

Le mécanisme .pth se déclenche au démarrage de tout processus Python, y compris pip. Dans les environnements CI/CD, la charge utile peut donc s’exécuter pendant les étapes de build, et pas seulement à l’exécution de l’application.

Détection : êtes-vous concerné ?

Étape 1 : vérifiez la version installée

pip show litellm | grep Version

Si le résultat indique 1.82.7 ou 1.82.8, considérez le système comme compromis et suivez les instructions de correction ci-dessous. Ne vous contentez pas de mettre à niveau : la charge utile a peut-être déjà été exécutée.

Étape 2 : recherchez des éléments de persistance

# Check for the sysmon backdoor
ls -la ~/.config/sysmon/sysmon.py 2>/dev/null && echo "BACKDOOR FOUND"
ls -la /root/.config/sysmon/sysmon.py 2>/dev/null && echo "ROOT BACKDOOR FOUND"

# Check for the systemd persistence service
systemctl --user status sysmon.service 2>/dev/null
ls -la ~/.config/systemd/user/sysmon.service 2>/dev/null && echo "PERSISTENCE SERVICE FOUND"

# Check for exfiltration archive remnants
ls /tmp/tpcp.tar.gz /tmp/session.key /tmp/payload.enc /tmp/session.key.enc 2>/dev/null && echo "EXFIL ARTIFACTS FOUND"

Étape 3 : recherchez des fichiers .pth malveillants

# Find .pth files in site-packages with suspicious patterns
find $(python3 -c "import site; print(' '.join(site.getsitepackages()))") \
  -name "*.pth" -exec grep -l "base64\|subprocess\|exec" {} \;

Étape 4 : vérifiez les hachages des fichiers

# Check proxy_server.py (1.82.7)
find / -path "*/litellm/proxy/proxy_server.py" 2>/dev/null -exec shasum -a 256 {} \;
# Malicious: a0d229be8efcb2f9135e2ad55ba275b76ddcfeb55fa4370e0a522a5bdee0120b

# Check litellm_init.pth (1.82.8)
find / -name "litellm_init.pth" 2>/dev/null -exec shasum -a 256 {} \;
# Malicious: 71e35aef03099cd1f2d6446734273025a163597de93912df321ef118bf135238

Étape 5 : recherchez des indicateurs réseau

grep "litellm.cloud\|checkmarx.zone" /etc/hosts
grep "models.litellm.cloud\|checkmarx.zone" /var/log/syslog 2>/dev/null

Étape 6 : vérifiez Kubernetes

kubectl get pods -A | grep "node-setup-"

Étape 7 : lancez une analyse avec Snyk

snyk test --package-manager=pip
Tableau de bord de sécurité Snyk affichant des indicateurs de dépôt, comme 61 % de dépôts testés, des dépôts inactifs et une liste de dépôts de classe A à haut risque présentant des vulnérabilités critiques.

Bannière d’alerte dans l’application Snyk, avec les dépôts concernés affichés dans l’inventaire des actifs. Le lien Trust Center permet d’accéder à l’état de l’incident en temps réel.

Les clients Snyk peuvent également consulter Snyk Advisor pour le package litellm et la fiche complète de vulnérabilité SNYK-PYTHON-LITELLM-15762713.

Correction

Si vous n’avez PAS installé la version 1.82.7 ou 1.82.8 :

Épinglez la version sur <=1.82.6 jusqu’à la disponibilité d’une version saine :

pip install "litellm<=1.82.6"
# requirements.txt:
litellm<=1.82.6

Si vous AVEZ installé la version 1.82.7 ou 1.82.8 :

La charge utile s’exécute au démarrage de Python, y compris lors de pip install. Considérez le système comme potentiellement compromis, que vous ayez exécuté du code applicatif ou non.

  1. Supprimez les éléments de persistance :

rm -f ~/.config/sysmon/sysmon.py
rm -f ~/.config/systemd/user/sysmon.service
systemctl --user disable sysmon.service 2>/dev/null
systemctl --user daemon-reload
rm -f /tmp/tpcp.tar.gz /tmp/session.key /tmp/payload.enc /tmp/session.key.enc /tmp/.pg_state /tmp/pglog
  1. Faites tourner les identifiants sur le système concerné :

  • Clés privées SSH : générez de nouvelles clés et révoquez les anciennes dans authorized_keys, GitHub et GitLab

  • Identifiants cloud : clés d’accès AWS, clés de compte de service GCP, principaux de service Azure

  • Clés API : fichiers .env, variables d’environnement du shell, secrets CI/CD

  • Identifiants de registre Docker : ~/.docker/config.json

  • Kubernetes : ~/.kube/config, jetons de compte de service dans le cluster

  • Mots de passe de base de données figurant dans les fichiers de configuration du système

  • Identifiants Git stockés dans ~/.gitconfig ou dans le gestionnaire d’identifiants du système

  • Phrases de récupération de portefeuilles de cryptomonnaies

  1. Auditez AWS Secrets Manager et SSM Parameter Store, car la charge utile les interroge directement si les métadonnées d’instance sont accessibles.

  1. Auditez les secrets du cluster Kubernetes. Si un jeton de compte de service était présent, tous les secrets de tous les espaces de noms ont peut-être été lus. Recherchez les pods node-setup-* dans kube-system.

  1. Installez une version saine dans un nouvel environnement au lieu de mettre à niveau l’installation existante :

pip install "litellm<=1.82.6"

Pourquoi la vérification des hachages par pip n’a rien détecté

La vérification des hachages confirme qu’un fichier correspond à celui annoncé par PyPI, mais ne permet pas de savoir si le contenu annoncé est malveillant.

Le fichier litellm_init.pth de la version 1.82.8 est correctement déclaré dans le fichier RECORD de la wheel, avec un hachage correspondant. pip install --require-hashes aurait abouti. Le package passe tous les contrôles d’intégrité habituels, car le contenu malveillant a été publié à l’aide d’identifiants légitimes : aucun hachage ne diffère, aucun domaine suspect n’est détecté et le nom du package ne comporte aucune faute.

La seule façon de détecter le problème lors de l’installation consiste à vérifier si un package installe des fichiers .pth et si ces fichiers contiennent des motifs tels que subprocess, base64 ou exec. Aucun plugin pip largement déployé ne le fait automatiquement à l’heure actuelle.

La tendance générale

Dans cette campagne, les cibles sont des outils qui disposent d’un accès étendu aux pipelines automatisés : un scanner de conteneurs (Trivy), un outil d’analyse de l’infrastructure (KICS) et une bibliothèque de routage de modèles d’IA (LiteLLM). Par conception, chacun de ces outils doit disposer d’un accès étendu aux systèmes sur lesquels il intervient (identifiants, configurations, variables d’environnement).

LiteLLM est de plus en plus déployé comme passerelle LLM centralisée stockant les identifiants API de plusieurs fournisseurs de modèles. Dans cette configuration, les identifiants accessibles depuis un seul hôte compromis sont plus nombreux que dans le cas d’une application classique.

La divulgation initiale s’est propagée dans les communautés de développeurs en IA (r/LocalLLaMA, r/Python, Hacker News) plutôt que par les canaux de sécurité traditionnels tels que r/netsec ou les flux CVE.

Pour en savoir plus sur les risques de la chaîne d’approvisionnement propres aux outils LLM, consultez l’article de Snyk Learn Vulnérabilités de la chaîne d’approvisionnement des LLM. L’attaque de la chaîne d’approvisionnement Ultralytics AI Pwn Request de 2024 constitue également un point de comparaison utile : une autre bibliothèque Python d’IA largement utilisée, compromise au moyen d’une faille CI/CD, selon une chaîne d’attaque similaire.

Indicateurs de compromission

Hachages de fichiers :

Fichier

SHA-256

litellm_init.pth (1.82.8)

71e35aef03099cd1f2d6446734273025a163597de93912df321ef118bf135238

proxy_server.py (1.82.7)

a0d229be8efcb2f9135e2ad55ba275b76ddcfeb55fa4370e0a522a5bdee0120b

sysmon.py

6cf223aea68b0e8031ff68251e30b6017a0513fe152e235c26f248ba1e15c92a

Réseau :

  • Exfiltration : https://models.litellm.cloud/ (POST)

  • Interrogation C2 : https://checkmarx.zone/raw (GET)

Système de fichiers :

  • ~/.config/sysmon/sysmon.py ou /root/.config/sysmon/sysmon.py

  • ~/.config/systemd/user/sysmon.service (description : « System Telemetry Service »)

  • /tmp/tpcp.tar.gz, /tmp/session.key, /tmp/payload.enc, /tmp/session.key.enc

  • /tmp/.pg_state, /tmp/pglog

Kubernetes :

  • Pods : node-setup-{node_name} dans kube-system

  • Nom du conteneur : setup, image : alpine:latest

Préfixe de la clé publique RSA (codé en dur dans les charges utiles des trois opérations) :

MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvahaZDo8mucujrT15ry+...

Mesures à prendre immédiatement

  1. Vérifiez votre version de litellm : pip show litellm | grep Version

  2. Épinglez la version sur <=1.82.6 dans tous les environnements

  3. Exécutez snyk test --package-manager=pip

  4. Si la version 1.82.7 ou 1.82.8 était installée : faites tourner les identifiants et recherchez des éléments de persistance

  5. Auditez les pipelines CI/CD pour repérer les versions d’outils non épinglées, notamment dans GitHub Actions

  6. Vérifiez Kubernetes : kubectl get pods -A | grep node-setup-

LIVRE BLANC

La crise de la sécurité de l’IA dans votre environnement Python

Avec l’accélération fulgurante du développement, savez-vous vraiment à quoi votre environnement d’IA peut accéder ?