Am 21. Oktober 2026 erschien LiteLLM 1.107.0 auf dem Python Package Index und in den offiziellen Container-Images. Das Release enthielt Schadcode. Nicht eine Zeile Anwendungscode musste LiteLLM importieren, damit er lief. Es reichte, den Python-Interpreter in einer Umgebung zu starten, in der das Paket installiert war. Deshalb ist das kein Fall für ein Paket-Downgrade: Die ganze Umgebung muss neu bewertet werden.
LiteLLM sitzt im Betrieb meist als Gateway zwischen Anwendungen und Modell-Anbietern und hält dabei eine Menge Geheimnisse: API-Schlüssel der Modellanbieter, Datenbank-Verbindungszeichenfolgen, Workload-Identitäten, Routing-Konfiguration, Mandanten-Richtlinien. Dasselbe Paket liegt aber auch auf Entwickler-Rechnern, Build-Agenten, Testsystemen und in Containern. Wer nur ans produktive Gateway denkt, übersieht die Hälfte der Fläche, auf der eine kompromittierte Version landen kann.
Laut den Maintainern von LiteLLM lief der Angriff über gestohlene Zugangsdaten. Gestohlene GitHub-Anmeldedaten eines Entwicklers sollen den Release-Workflow so verändert haben, dass er statt auf das legitime Repository BerriAI/litellm auf das ähnlich benannte Berri-AI/litellm zeigte. Ein weiteres Konto lieferte anschließend die Freigabe für den Produktionsschritt. Microsoft hat nach eigener Aussage nicht unabhängig festgestellt, wie der Angreifer ursprünglich an die Umgebung und die Daten gelangte. Für die Bewertung des eigenen Risikos ist das zweitrangig. Entscheidend ist, dass der Veröffentlichungsprozess das Ziel war und nicht das einzelne Paket.
Startup-Hooks: warum ein Import nicht nötig war
Beim Start arbeitet Python ein paar Dinge ab, bevor der erste eigene Programmcode läuft. Es führt Import-Anweisungen in Dateien mit der Endung pth aus und versucht danach, ein Modul namens sitecustomize zu importieren. Für die meisten Entwickler ist das ein Randdetail der Sprachdokumentation. Für einen Angreifer ist es ein Automatismus, der zuverlässig anspringt, bevor die Anwendung überhaupt weiß, dass sie läuft.
Die kompromittierte Wheel-Datei brachte genau solche Auslöser mit: cook.pth, cook.py und sitecustomize.py, abgelegt unter litellm-1.107.0.data/purelib/. Bei der Installation landeten diese Dateien im Bibliotheksverzeichnis der Python-Umgebung, also außerhalb des LiteLLM-Anwendungsverzeichnisses. Das ist der entscheidende Punkt. cook.pth enthält eine einzige ausführbare Zeile: den Import von cook. Das lädt cook.py, dessen Wrapper ein eingebettetes Programm Base64-dekodiert, mit zlib entpackt und ausführt. Das Programm nennt sich selbst PRIVCOLLECT.
Vor der Sammlung prüft der Collector die Umgebungsvariable PRIVCOLLECT_ACTIVE. Ist sie gesetzt und nicht leer, bricht er ab. Andernfalls setzt er sie und startet die Sammlung in einem Daemon-Thread, damit abgeleitete Prozesse die Arbeit nicht doppelt machen. Nach einem Upload mit HTTP-Status 200 versucht er, die Markierungsdatei /tmp/.pc_once_72129118c59f5636c0bb_ zu schreiben. Beide Sperren sind keine Beruhigung. Ein kurzlebiger Prozess kann sich beenden, bevor der Thread fertig ist, und ein gescheitertes Schreiben hinterlässt keine Spur. Fehlende Marker heißen also nicht, dass nichts passiert ist.
Der zweite Pfad läuft über sitecustomize.py und zielt auf Unix-Systeme. Er bringt einen eigenen Collector mit und kann zusätzlich Python-Code von einem entfernten Host nachladen und ausführen. Beim Start nimmt der Loader eine exklusive Sperre auf /tmp/remote_code_loader.lock, damit pro Host nur eine Instanz läuft, und arbeitet im Hintergrund. In jedem Zyklus liest er eine auf GitHub gehostete Datei domain.txt, die als veränderbarer Zeiger auf einen Angreifer-Server dient. Liefert sie eine gültige, auflösbare Adresse, ruft der Loader den Pfad codepy ab, kompiliert die Antwort und führt sie als Python-Code aus, ohne Signaturprüfung. Ob dieser Pfad in den Beobachtungen tatsächlich Code nachgeladen hat, ist nicht bestätigt.
Was der Collector angefasst hat
Der Sammelteil arbeitet mehrere Module nacheinander ab, jedes mit eigener Fehlerbehandlung. Ein fehlendes Ziel stoppt die übrigen also nicht. Bei Entwickler- und KI-Werkzeugen las der Collector ausgewählte Authentifizierungs- und Konfigurationsdateien von KI-Assistenten, IDEs, GitHub Copilot, VS Code und MCP-Integrationen: feste Pfade und JSON-Dateien auf oberster Ebene, begrenzt durch ein Sammlungsbudget. Bei Quellcode-Verwaltung und Paket-Ökosystemen suchte er nach Git-Zugangsdaten, Paketmanager-Konfigurationen, einzelnen dotenv-Dateien und Dateien mit privaten Schlüsseln; flache Verzeichnisdurchläufe und Größenlimits engten die Suche ein.
Breiter wurde es bei Cloud- und Workload-Zugangsdaten. Der Collector las AWS-, Azure-CLI- und Google-Cloud-Konfigurationen und Credential-Caches, Kubernetes-Service-Account-Dateien, Docker-Konfiguration und eingebundene Secrets. Zusätzlich versuchte er, die Metadaten-Endpunkte von AWS und GCP anzusprechen. Bei Browsern griff er auf credential-relevante Daten der Chromium-Familie und von Firefox zu, außerdem auf Verläufe, Shell-, Datenbank-Client- und Werkzeug-Historien. Ob Passwörter tatsächlich entschlüsselt wurden, hing von Plattform und Format ab; der Code allein belegt keinen Erfolg.
Auch die Umgebung selbst war ein Ziel. Der Collector sammelte seine eigene Prozessumgebung ein und unter Linux zusätzlich die Umgebungen anderer Prozesse, soweit sie über procfs lesbar waren. So können Geheimnisse in Reichweite geraten, die für den LiteLLM-Prozess nie vorgesehen waren. Dazu kamen lesbare SSH-Dateien sowie ausgewählte Konfigurationen von GPG, TLS, CI/CD, Vault und Infrastruktur-Werkzeugen. Und der LiteLLM-Proxy selbst: Wenn die Voraussetzungen erfüllt waren, startete der Collector einen Python-Kindprozess mit Prisma und las ausgewählte Felder aus zwei Tabellen der Proxy-Datenbank, je maximal 500 Zeilen. Wer solche Datenbanken betreibt, sollte diese Zahl im Kopf behalten.
Verpackt, verschlüsselt, abgeschickt
Die Ergebnisse der Module und die Host-Metadaten wurden als JSON serialisiert. Pro Lauf erzeugte der Collector einen 32 Byte langen AES-Schlüssel und einen 16 Byte langen Initialisierungsvektor, verschlüsselte das Dokument mit AES-256-CBC und verpackte den Schlüssel mit einem eingebetteten 4096-Bit-RSA-Public-Key nach OAEP. Der OpenSSL-Pfad versucht zuerst SHA-256 und fällt dann auf SHA-1 zurück; welcher Digest verwendet wurde, steht in den Metadaten. Für die Forensik ist das nützlich.
Ausgeliefert wurde ein gzip-komprimiertes tar-Archiv mit drei Mitgliedern: data.bin mit Initialisierungsvektor und verschlüsselter Sammlung, key.bin mit dem eingepackten AES-Schlüssel und hdr.json mit Klartext-Metadaten zur Hülle. Danach versuchte der Collector einen HTTP-POST an hxxp://www[.]litellm[.]dev/healthcheck, mit Content-Type application/octet-stream, einem festen, browserähnlichen User-Agent und einer Prüfsumme des Bodys im Header X-SHA256. Die Upload-Routine wiederholt den Versuch und wertet nur HTTP 200 als Erfolg. Das OpenSSL-Backend legt temporäre Verzeichnisse mit dem Präfix .pc_e__ an, stellt Ein- und Ausgabedateien dort ab und räumt sie danach weg.
Prüfen, ohne den Auslöser erneut zu ziehen
Ein Satz aus der Analyse ist für die Reaktion entscheidend: Ermittler sollten Python oder pip in einer verdächtigen Umgebung nicht starten, um festzustellen, ob das schädliche Paket vorhanden ist. Jeder Start kann den Collector erneut auslösen. Untersucht man stattdessen das Dateisystem von außen, etwa über ein gemountetes Image oder einen separaten Analyse-Rechner, bleibt der Auslöser unberührt. In einem dokumentierten Windows-Fall genügte ein routinemäßiger Python-Aufruf, um eine unbeteiligte Abhängigkeit zu installieren – und dieser Aufruf aktivierte den Collector.
Ebenso wichtig ist, was nach dem Aufräumen übrig bleibt. Das Entfernen des Pakets oder eines Release-Tags reicht nicht. Die Startup-Dateien lagen außerhalb des LiteLLM-Anwendungsverzeichnisses und überleben eine Deinstallation; wer nur das Paketverzeichnis löscht, lässt sie liegen. Hinzu kommen laufende Prozesse, die den Daemon-Thread oder den Unix-Loader schon gestartet haben, und all jene Zugangsdaten, die die kompromittierte Identität erreichen konnte. Beobachtungen auf Windows und macOS deckten sich in Zielsuche, Verschlüsselung und ausgehender Verbindung mit dem rekonstruierten Collector – ein Muster, an dem sich Endpoint-Telemetrie, Detection-Regeln und gezielte Suchabfragen ausrichten lassen.
Was das für den Betrieb bedeutet
Entscheidend ist nicht die Versionsnummer 1.107.0, sondern dass die Vertrauensgrenze nicht beim Import eines Pakets verläuft, sondern beim Start des Interpreters. Wer eine Abhängigkeit pinnt, schützt sich gegen ungewollte Upgrades, aber nicht gegen ein Release, das im offiziellen Kanal selbst vergiftet wurde. Wer Build-Agenten und Entwickler-Rechner als Nebenschauplatz betrachtet, übersieht die Systeme mit den interessantesten Schlüsseln. Und wer einen solchen Vorfall als reines Paketproblem behandelt, lässt genau die Stellen stehen, die beim nächsten Python-Aufruf wieder anspringen.
Für die Praxis: Umgebungen inventarisieren, in denen LiteLLM 1.107.0 installiert war, diese Umgebungen ohne Python-Start prüfen, verbliebene Prozesse beenden und erreichbare Geheimnisse rotieren – in der Reihenfolge, in der sie Schaden anrichten können: zuerst Cloud- und Workload-Identitäten, dann Paket- und Repository-Zugänge, dann alles, was in Umgebungsvariablen und Historiendateien liegt. Prüfsummen und Signaturen helfen nur, wenn jemand sie prüft und der Prüfende nicht derselbe gestohlene Account ist.
Quelle: microsoft.com
