Attackers hijacked LiteLLM's release workflow (suspected leaked GitHub CLI OAuth token from an exposed internal deployment's logs) and published a backdoored 1.107.0 to PyPI, Docker Hub and GHCR on October 21. A cook.pth hook runs the "privcollect" credential stealer at every Python start on the host, even when litellm is never imported; sitecustomize.py forks an hourly remote-code loader.
Verdict: if 1.107.0 was installed, assume compromise — isolate, rebuild, rotate. This is not a patch decision; the exposure window is narrow (about two hours on PyPI, under three on Docker) but anything inside it ran a credential stealer with host-wide reach, and stolen AWS credentials were confirmed abused ~4.5 hours later (STS validation, then Bedrock enumeration — LLMjacking). Rotation scope is everything the host could see: cloud keys, browser-stored credentials, tokens in env vars and config.
Seven wheels (Linux/macOS/Windows) each install three files into site-packages: cook.pth (loads at interpreter start), cook.py (privcollect — browser, AWS, GCP, Azure credentials; exfil as tar.gz POSTed to litellm[.]dev/healthcheck, a lookalike domain), and sitecustomize.py (Linux/macOS only: forks a daemon holding /tmp/remote_code_loader.lock that hourly fetches a C2 domain from a GitHub dead drop and executes <domain>/codepy). Socket's scanner flagged all seven wheels as malicious about 50 minutes after upload; PyPI pulled the release at 13:23 UTC.
pip install litellm between 11:39–13:23 UTC, Oct 21sitecustomize.py is Linux/macOS only) — but not the credential stealer| Category | Source | Status |
|---|---|---|
| Vendor | BerriAI / LiteLLM | Acted — releases pulled from PyPI/Docker/GHCR; incident issue #47826 with RCA (suspected GitHub CLI OAuth token leak); vendor says assume compromise if 1.107.0 was installed; pin 1.106.2 by digest |
| Package scanners | Socket / OSV | Flagged fast — Socket marked all seven wheels malicious ~50 min after upload; OSV carries malicious-package entry MAL-2026-2144, so osv-scanner / pip-audit flag installed copies. The rare case where SCA is the detection — the opposite of a runtime CVE |
| Research | StepSecurity / SafeDep / Wiz | Published — payload analysis (privcollect naming, file set, dead-drop mechanics), hashes and IoCs |
| Cloud | CloudTrail | Huntable — stolen AWS creds validated with sts:GetCallerIdentity then bedrock:ListFoundationModels/IAM enumeration from attacker IPs, Oct 21 16:20–17:01 UTC |
| VM scanners / EDR | Qualys / Tenable / EDR vendors | Nothing published — no CVE to scan for; coverage is file-IoC sweeps and the package-scanner lane above |
pip list fleet-widecook.pth / cook.py / sitecustomize.py in site-packages and /tmp/remote_code_loader.lockPublished guidance above covers the artifacts; these are fleet-sweep translations.
# host sweep (any config mgmt / EDR file search) find-maxdepth 2 -name 'cook.pth' -o -name 'cook.py' test -f /tmp/remote_code_loader.lock # Linux/macOS: loader marker # container registries: scan image layers for the three filenames in site-packages
# egress dest.domain = litellm.dev OR dest.url contains raw.githubusercontent.com/letters9813 → any hit since Oct 21 11:39 UTC is a confirmed infection, not a maybe
Splunk (index=*) / Elastic ES|QL (FROM logs-*) — CIM Endpoint / Network Resolution / Web field names for Splunk, ECS for Elastic; tune to your add-on or integration:
# host artifacts (EDR / Sysmon file events, FIM)
index=* earliest=-30d@d (file_name IN ("cook.pth","cook.py","sitecustomize.py") OR file_path="/tmp/remote_code_loader.lock")
| where file_path="*site-packages*" OR file_path="/tmp/remote_code_loader.lock"
| stats count values(file_path) as paths min(_time) as first by host
# egress: exfil domain and the GitHub dead drop
index=* earliest=-30d@d (query="litellm.dev" OR query="*.litellm.dev" OR dest="litellm.dev" OR url="*raw.githubusercontent.com/letters9813*")
| stats count min(_time) as first max(_time) as last by src host
# package install evidence (CI / build logs, pip caches)
index=* earliest=-30d@d ("litellm==1.107.0" OR "litellm-1.107.0" OR "litellm:1.107.0")
| stats count by host source
// host artifacts (EDR / Sysmon file events, FIM)
FROM logs-*
| WHERE @timestamp > NOW() - 30 days
| WHERE file.name IN ("cook.pth", "cook.py", "sitecustomize.py") OR file.path == "/tmp/remote_code_loader.lock"
| WHERE file.path LIKE "*site-packages*" OR file.path == "/tmp/remote_code_loader.lock"
| STATS count = COUNT(*), paths = VALUES(file.path), first = MIN(@timestamp) BY host.name
// egress: exfil domain and the GitHub dead drop
FROM logs-*
| WHERE @timestamp > NOW() - 30 days
| WHERE dns.question.name == "litellm.dev" OR dns.question.name LIKE "*.litellm.dev" OR destination.domain == "litellm.dev" OR url.full LIKE "*raw.githubusercontent.com/letters9813*"
| STATS count = COUNT(*), first = MIN(@timestamp), last = MAX(@timestamp) BY source.ip, host.name
// package install evidence (CI / build logs, pip caches)
FROM logs-*
| WHERE @timestamp > NOW() - 30 days
| WHERE message LIKE "*litellm==1.107.0*" OR message LIKE "*litellm-1.107.0*" OR message LIKE "*litellm:1.107.0*"
| STATS count = COUNT(*) BY host.name, log.file.path
sts:GetCallerIdentity) and Bedrock enumerated — confirmed abuseLiteLLM incident issue #47826 (vendor) · The Hacker News · THN — token-leak RCA · StepSecurity · Requesty — timeline · SafeDep