.pth file in site-packages made each Python interpreter startup spawn a second Python process on the runner. In the runtime record, that double-Python ancestry dropped into a shell and began credential-harvest commands before any explicit litellm import path was exercised.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 (Snyk write-up) — 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. The command set clusters into four phases.
Recon first: hostname, whoami, uname -a, ip addr, then printenv for full environment capture.
Cloud credential probes next: environment grep for AWS_, Azure/GCP key checks, and curls to 169.254.169.254 and 169.254.170.2. Both probes triggered Garnet's net_suspicious_tool_exec signal. A PyPI package has no legitimate reason to query cloud metadata services at interpreter startup.
Kubernetes and filesystem harvest in parallel: grep for kube and k8s, run kubectl get secrets --all-namespaces -o json, then scan /var/secrets, /run/secrets, and workspace files for API keys, webhook URLs, wallet RPC credentials (rpcuser, rpcpassword), database connection strings, Solana wallets, and WireGuard configurations. The lineage captures each child process off the .pth detached branch.
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.This run starts in CI, but it illustrates the wider untrusted-execution surface: code paths you did not author gaining privileged runtime access by default. Agent-generated execution and transitive package startup hooks share the same failure mode. Kernel-level ancestry and egress context are what distinguish legitimate automation from hostile runtime behavior.
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. The same RSA key and exfiltration headers appeared days later in the Telnyx PyPI compromise, confirming a single actor operating across multiple packages in rapid succession.
Explore the run profile above, or start observing your own workflows with Garnet.