From One Stolen Token to 50 Compromised Packages: Anatomy of the TeamPCP Supply Chain Attack

It started with a pull_request_target misconfiguration in a GitHub Actions workflow. Within eight days, a single stolen credential had cascaded into the compromise of Aqua Security's Trivy, Checkmarx's KICS, 50+ npm packages, and BerriAI's LiteLLM — a Python library embedded in roughly 36% of cloud environments. The attacker group, tracked as TeamPCP, didn't need a zero-day. They needed one token and patience.

Here's how it unfolded.

March 19: The Trivy Breach

The entry point was Aqua Security's Trivy vulnerability scanner. Attackers exploited a GitHub Actions pull_request_target trigger — a notoriously dangerous workflow event that runs with write permissions on the base repository, even when triggered by a fork's pull request. Through this, they obtained a Personal Access Token with publish rights.

Within hours, malicious Trivy v0.69.4 was live. Attackers force-pushed 76 of 77 aquasecurity/trivy-action tags. The injected code dumped GitHub Runner.Worker process memory, harvested credentials from standard locations, encrypted the haul with AES-256 and RSA-4096, and exfiltrated everything to scan.aquasecurtiy[.]org — a typosquatted domain one transposed letter away from the real thing.

The credentials harvested from Trivy's CI/CD pipelines included npm tokens. That's where things got significantly worse.

March 20–22: CanisterWorm Spreads Across npm

Armed with stolen npm tokens, TeamPCP deployed what researchers named CanisterWorm — a self-propagating package worm. The mechanism was brutally efficient:

  1. A poisoned postinstall hook in package.json executes automatically on npm install

  2. The script authenticates with each stolen npm token it finds in the environment

  3. It enumerates every package that token has publish access to

  4. It bumps the patch version, fetches the original README to keep appearances, and republishes with the worm payload baked in

Twenty-eight packages in @EmilGroup, sixteen in @opengov, and several others across @teale.io, @airtm, and @pypestream were compromised. Each newly published package stole more tokens, which compromised more packages. A classic worm loop.

The command-and-control infrastructure was notably resilient. Rather than conventional servers, CanisterWorm polled an ICP (Internet Computer Protocol) canister — decentralized blockchain infrastructure with no single host to take down via abuse reports. When dormant, the C2 returned YouTube URLs as a heartbeat signal. When active, it pushed arbitrary payloads.

Persistence came through a systemd service masquerading as PostgreSQL monitoring tooling at ~/.local/share/pgmon/service.py, configured with Restart=always and polling the C2 endpoint every 50 minutes.

March 24: LiteLLM Gets Hit

By March 24, a LiteLLM maintainer's PyPI credentials — likely harvested through the Trivy pipeline compromise — gave TeamPCP access to publish Python packages.

Version 1.82.7 injected base64-encoded malicious code directly into litellm/proxy/proxy_server.py. Anyone importing the proxy module triggered the payload. But version 1.82.8, published the same day, was far more insidious: it included a litellm_init.pth file in the package wheel.

Python automatically executes .pth file contents during interpreter initialization. You didn't need to import litellm. Simply running python --version in an environment where the package was installed was enough.

The credential harvesting was exhaustive. The malware collected SSH keys, AWS credentials (including ECS link-local endpoints, EC2 IMDSv2 token flows, IAM role credentials, Secrets Manager values, and SSM Parameter Store entries), Kubernetes service account tokens, .env files across directories, Git credentials, Docker configs, Terraform state files, and even cryptocurrency wallet keypairs.

Everything was encrypted with AES-256-CBC using PBKDF2 key derivation, wrapped in RSA-4096, and exfiltrated as tpcp.tar.gz to models.litellm[.]cloud. Forensic recovery of what was stolen: impossible without the attacker's private key.

If the malware detected a Kubernetes service account token — meaning it was running inside a pod — it escalated from theft to cluster takeover, spinning up privileged node-setup-* DaemonSets with host filesystem mounts across all nodes.

March 27: Telnyx Falls Too

Three days later, Telnyx PyPI versions 4.87.1 and 4.87.2 were compromised using the same playbook. The twist: the second-stage payload was embedded as XOR-encrypted code hidden within audio frames of a WAV file downloaded from a hardcoded IP. Steganography in a phone SDK package. The same RSA public key tied it back to TeamPCP.

The Window Was Small. The Blast Radius Wasn't.

The malicious LiteLLM packages were live on PyPI for approximately five hours — from 10:39 to 16:00 UTC on March 24. PyPI quarantined the project after discovery. LiteLLM's team rotated credentials, engaged Google Mandiant for forensics, and paused all new releases pending a full supply-chain review.

Five hours sounds short. But LiteLLM sees millions of downloads. Any CI/CD pipeline running pip install litellm without pinned versions during that window pulled the compromised package. Any Docker build. Any fresh virtual environment. And the .pth persistence mechanism means the malware may still be active in environments that installed 1.82.8, even if the package was subsequently "upgraded" — litellm_init.pth can persist in site-packages after version changes.

What This Actually Means for Your Team

The TeamPCP campaign exploits a fundamental weakness in how the open-source ecosystem handles trust: a publish token is a skeleton key. There is no two-party approval for package releases on npm or PyPI. One compromised credential, and the attacker is the maintainer.

Immediate actions if you're potentially affected:

  • Search for litellm_init.pth in all Python site-packages directories and remove it

  • Look for ~/.config/sysmon/sysmon.py, ~/.local/share/pgmon/service.py, and any node-setup-* Kubernetes pods

  • Check outbound DNS and traffic logs for models.litellm[.]cloud, checkmarx[.]zone, and scan.aquasecurtiy[.]org

  • Rotate every credential accessible from affected runtimes — don't treat uninstalling the package as complete remediation

Structural defenses going forward:

  • Pin dependency versions in CI/CD and use lockfiles. pip install litellm with no version constraint is the reason this attack works at scale

  • Audit GitHub Actions workflows for pull_request_target triggers — if your workflow checks out PR code and runs it with repo write permissions, you have the same vulnerability that started this entire chain

  • Restrict PyPI and npm token scopes. Use short-lived, per-package tokens rather than org-wide publish credentials

  • Monitor for .pth file creation in Python environments — there are very few legitimate uses for this mechanism

  • Apply least-privilege to Kubernetes service accounts. A pod that can create DaemonSets across all nodes shouldn't be running your LLM proxy

Reverting to a clean package version is step one. It is not the last step. If the malware executed, assume credential compromise and work backward from there.