LiteLLM provides a unified interface to 100+ LLM APIs, with roughly 95 million monthly downloads. TeamPCP used a PYPI_PUBLISH token — exfiltrated from LiteLLM's CI via the compromised Trivy action — to publish versions 1.82.7 and 1.82.8. Version 1.82.7 injected a payload into proxy_server.py that executes at import time. Version 1.82.8 added a litellm_init.pth file that fires on every Python interpreter startup — even if litellm is never imported — running a three-stage payload: credential harvesting across 50+ secret categories, AES-256-CBC + RSA-4096 encrypted exfiltration to models.litellm[.]cloud, and a persistent backdoor polling checkmarx[.]zone for second-stage payloads. A bug in the payload — a recursive fork bomb caused by the .pth re-firing on every spawned subprocess — is what first alerted the community. Sonatype detected and blocked the versions within seconds. PyPI quarantined the project within three hours.
What Garnet observed
Method: detonation of the litellm_init.pth payload from version 1.82.8 inside a GitHub Actions runner instrumented with Garnet's eBPF sensor.
The attack chain
Execution lineage
Run 23613262288 · jadoonf/pypi-analysis-feed
Analyse local PyPI archives
1.82.7 | 1.82.8 (profiled) | |
|---|---|---|
| Trigger | proxy_server.py at import | litellm_init.pth on interpreter startup |
| Visibility to static tools | Malicious module path | Site-packages .pth — often missed |
| Scope | Downstreams that import litellm | Any Python process in the environment |
The .pth file fires on interpreter initialization, not import. Static tools see a base64 string. At the kernel level, what Garnet sees is a Python interpreter spawning a second Python interpreter — python3.11 → python3.11 — which immediately drops to a shell. That double-python ancestry is the behavioral fingerprint of .pth auto-execution, and it should never appear during a package install.
From that shell, the payload executed 17 commands in rapid sequence. Garnet captured every one. Here's what happened, in order:
First, the payload fingerprints the machine — hostname, whoami, uname -a, ip addr — then dumps the entire environment with printenv. This is the recon stage: figure out what you're running on before you start stealing.
Then it goes after cloud credentials, fast. It greps the environment for AWS_, checks Azure, Google Cloud, and GCP service account keys. It probes both AWS metadata endpoints — the EC2 Instance Metadata Service at 169.254.169.254 and the ECS container credential endpoint at 169.254.170.2 — via curl. Both triggered Garnet's net_suspicious_tool_exec signal. There is no reason a PyPI package should be curling cloud metadata.
In parallel, it goes after Kubernetes. It greps for kube and k8s environment variables, then runs kubectl get secrets --all-namespaces -o json — attempting to dump every secret in any reachable cluster. Garnet recorded the kubectl connection to localhost:8080, probing for a K8s API server.
Then the filesystem sweep: find /var/secrets /run/secrets looking for mounted secrets, grep across the workspace for API keys, Slack and Discord webhook URLs, and cryptocurrency wallet RPC credentials (rpcuser, rpcpassword). It checks for database connection strings, Solana wallets, and WireGuard VPN configurations. The full sequence of commands is visible in the lineage above — every child process off the .pth detached child, annotated with the system recon, cloud credential probes, Kubernetes dump, and filesystem harvest.
Garnet recorded 65 behavioral events across the run. The payload-triggered signals — shell spawns from the base64 bootstrap, the IMDS and ECS network probes, credential file access on the crypto wallet searches, webhook URL sweeps across the workspace, and execution from /tmp — correlate into a single pattern: a credential stealer systematically working through the CI environment.
Both C2 domains (models.litellm[.]cloud and checkmarx[.]zone) have since been taken down — DNS returns NXDOMAIN — so the exfiltration and persistence stages do not complete. In the original attack, the harvested data would be bundled into tpcp.tar.gz, encrypted with AES-256-CBC + RSA-4096, and POSTed to the C2, with a persistent backdoor polling for second-stage payloads.
.pth auto-execution mechanism fires on interpreter startup, not import — it bypasses static analysis entirely. The payload's own fork-bomb behavior is what first alerted defenders. Runtime observation at the kernel level captures the full credential harvest and flags the anomalous process chain before exfiltration completes.Real-world impact
Garnet's dependents analysis identified 18 high-download packages — including databricks-agents (5.2M/mo), dspy (5.1M), and opik (3.8M) — with open version constraints that would have resolved to the compromised releases. Because LiteLLM sits between applications and LLM providers, a single compromised environment exposes API keys for every provider routed through the proxy.
Explore the run profile above, or start observing your own workflows with Garnet.