By UNST33R

LiteLLM 1.107.0 backdoor: attack analysis and a self-audit checklist

A malicious LiteLLM 1.107.0 release on PyPI and Docker shipped a credential stealer with dormant C2, built from stolen GitHub OAuth tokens. What the payload does, how to check your fleet, and why the code reads like LLM output.

On October 21, 2026, a trojanized LiteLLM 1.107.0 hit PyPI and Docker before being quarantined the same afternoon. Wiz Research documents a credential stealer plus a dormant second-stage loader running before any LiteLLM code. The last clean release is v1.106.2.

LiteLLM operators had a rough 2026: September brought authentication bypass to cloud compromise, and Check Point found exposed instances handing over admin control earlier this month. This one cuts deeper: the package itself was the weapon.

Entry path and timeline

Time (UTC, Oct 21)Event
08:56–10:46Attacker pushes 10 commits from hyphenated lookalike org Berri-AI (real: BerriAI): persistence files, version bump to 1.107.0, a data = "cook" directive in pyproject.toml bundling the payload into the wheel, and COPY cook ./cook in the Dockerfiles
11:39Trojanized 1.107.0 live on PyPI; Docker Hub/GHCR tags v1.107.0, latest, main-latest, main-stable follow
13:30–13:53Dead-drop repo taken down by GitHub; PyPI quarantines the wheel; Docker tags removed

Entry vector: stolen GitHub CLI OAuth tokens on three engineers' accounts, one triggering project-releaser, another approving it. All revoked.

What the payload does

ArtifactBehavior
cook.pth + cook.py (site-packages root)Executes on every Python interpreter start; steals credentials, encrypts them (AES-256-CBC, RSA-4096-wrapped key), exfiltrates to litellm[.]dev/healthcheck
sitecustomize.pyLinux/macOS only. Polls a GitHub dead-drop for a C2 domain, then fetches and runs a second stage (/codepy), on an hourly loop. The dead-drop was never populated, so this path stayed dormant

The stealer (privcollect, 16 modules) targets browser passwords and cookies, AWS (~/.aws, env vars, IMDS), GCP service-account keys, Azure CLI tokens, SSH and GPG keys, shell history, and OpenAI/Anthropic/Cohere API keys. It also queries the LiteLLM Proxy database directly: SELECT * FROM "LiteLLM_CredentialsTable" LIMIT 500, harvesting every upstream provider key. The .pth + sitecustomize.py dual persistence is a first in the wild.

Audit checklist

pip show litellm                                   # 1.107.0 = assume compromise
ls site-packages/cook.pth cook.py sitecustomize.py # payload trio
ls /tmp/.pc_once_72129118c59f5636c0bb              # infection marker
env | grep PRIVCOLLECT_ACTIVE                      # payload indicator

Also grep DNS and egress logs for litellm.dev. If anything matches: rotate every provider key in the Proxy database, cloud credentials on the host, browser profiles, and all GitHub CLI tokens.

IndicatorValue
Exfil endpointlitellm[.]dev/healthcheck, hardcoded X-Token: XWiA8gZNa7NcX2XRoAEb, IE7 user-agent
Marker / env/tmp/.pc_once_72129118c59f5636c0bb; PRIVCOLLECT_ACTIVE=1
Wheel SHA256 (manylinux_2_28_x86_64)462d0c9f67ca522e0e4acd245e90b36ce0d5353604a66bb7c43a1ec4a6fcf7

Full per-platform wheel, image, and payload hashes are in the Wiz report.

Malware written by prompt

The code carries strong LLM fingerprints: verbose names (encrypted_private_key_output), generic try/except wrappers everywhere, silent failures on untested platforms (the /tmp/ marker path breaks on Windows; fcntl/fork crash the loader), and a defined-but-never-called exec_py module. Wiz's read: iterative prompting produced something functional on the happy path and brittle at the edges. The same day, frontier models turned a decade-old PHP memory bug into a working exploit (our PHP2Shell analysis). October 21, 2026 put AI on both sides of the table: attacker tooling and vulnerability research.

LiteLLM-format clients survived this incident only when pinned below 1.107.0. Pointing LiteLLM-compatible tools at a gateway with its own release chain (example setup) takes PyPI out of the trust path. Get an API key or review the acceptable-use policy.