← Verdict cards Cutsign
Hunt + rotate Confirmed credential abuse 2h exposure window No CVE — OSV MAL-2026-2144 as of 2026-10-23

LiteLLM 1.107.0 supply chain compromise

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.

30-second read

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.

Exposed if

  • Unpinned pip install litellm between 11:39–13:23 UTC, Oct 21
  • Docker/GHCR pull of v1.107.0, latest, main-latest, main-stable between 11:46–14:20 UTC, Oct 21
  • Any CI runner, dev laptop or build host that resolved the package in those windows — the stealer runs on every Python start, litellm imported or not

Not exposed if

  • Pinned to 1.106.2 (by digest) or anything other than 1.107.0
  • Installs resolved outside the windows (release was pulled)
  • Windows hosts escape the hourly loader (sitecustomize.py is Linux/macOS only) — but not the credential stealer

Detection coverage — as of Oct 23

CategorySourceStatus
VendorBerriAI / LiteLLMActed — 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 scannersSocket / OSVFlagged 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
ResearchStepSecurity / SafeDep / WizPublished — payload analysis (privcollect naming, file set, dead-drop mechanics), hashes and IoCs
CloudCloudTrailHuntable — stolen AWS creds validated with sts:GetCallerIdentity then bedrock:ListFoundationModels/IAM enumeration from attacker IPs, Oct 21 16:20–17:01 UTC
VM scanners / EDRQualys / Tenable / EDR vendorsNothing published — no CVE to scan for; coverage is file-IoC sweeps and the package-scanner lane above

Do now

  • Inventory: anywhere litellm resolved on Oct 21 — lockfiles, CI caches, image layers, pip list fleet-wide
  • Sweep hosts for cook.pth / cook.py / sitecustomize.py in site-packages and /tmp/remote_code_loader.lock
  • Hit = isolate, rebuild from clean base, rotate every credential the host could reach
  • Check egress logs for litellm[.]dev and the GitHub dead drop (letters9813)
  • CloudTrail: the STS → Bedrock pattern from the listed IPs since Oct 21

Escalation triggers

  • Any host artifact or egress hit → incident lane, full credential rotation
  • The hourly loader having fetched anything (dead drop was live) → treat as hands-on-keyboard
  • Further releases or other packages tied to the same stolen token
  • Bedrock/LLM usage anomalies on any potentially exposed AWS account

Inferred detections — while you wait

Published 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

Timeline — Oct 21–22 (UTC)

Sources

LiteLLM incident issue #47826 (vendor) · The Hacker News · THN — token-leak RCA · StepSecurity · Requesty — timeline · SafeDep

Public OSINT only — contains no internal data Marcus Liberto · source Lane and triggers per CIRT triage-flow draft Tracked story: Cutsign 10-22 (deep read)