BLOG

TeamPCP Supply Chain Campaign: A March 2026 Retrospective

TeamPCP executed a cascading multi-phase supply chain attack that started with a single unrevoked credential stolen from Trivy's CI pipeline.

By cb482791-4ef1-4762-96ad-b0ca4bdd538e ·

TeamPCP Supply Chain Campaign: A March 2026 Retrospective

On March 19, 2026 I got a one line note from OpenSourceMalware head researcher Paul McCarty. It read: "Ouch. Trivy compromised." That signaled the start of this year's biggest software supply chain attack to date. While much of the security world was headed to BSidesSF and RSA Conference, the threat actor group TeamPCP executed a series of attacks that left vendors and defenders scrambling.

We covered stages of these attacks as they happened (in TeamPCP Defaces Aqua Security's Internal GitHub Org — 44 Repos Exposed and TeamPCP Hijacks LiteLLM's PyPI Package — Credential Stealer Hits 40k-Star Project). This article serves as a retrospective on the attack.

Use the #TeamPCP to see all the associated threat reports.

Who is TeamPCP?

TeamPCP (also tracked as DeadCatx3, PCPcat, ShellForce) is a financially motivated, cloud-native threat actor with a documented history targeting Docker APIs, Kubernetes clusters, and open-source ecosystems for credential theft, ransomware deployment, and cryptomining. The group has been linked to CipherForce, a ransomware and extortion operation, indicating that the CI/CD credential harvests from this campaign feed a broader monetization chain. TeamPCP has been tracked by Aikido Security, Socket, Wiz, Flare, and others across campaigns spanning 2024 into 2026.

The March 2026 campaign is their most sophisticated operation on record. Five vendor ecosystems were compromised in roughly five days, all traceable to a single credential that was never fully revoked after a February incident.

TeamPCP is evolving

TeamPCP's trajectory over the past 18 months shows a group systematically upgrading its capability at each stage.

In 2024, the group focused on opportunistic exploitation of misconfigured Docker APIs, Kubernetes clusters, Ray dashboards, and Redis servers for credential theft and cryptomining. By mid-2025 they had moved into supply chain territory, building tooling to harvest CI/CD credentials at scale. In late 2025 they deployed CanisterWorm, a self-propagating worm that used ICP Canister nodes as decentralized, censorship-resistant C2 infrastructure — the first observed use of this technique in the wild. In early 2026, destructive payloads with geotargeting logic appeared, combining credential theft with region-specific destruction. The March 2026 campaign represents the culmination: a cascading chain through five vendor ecosystems seeded by a single retained credential.

The role of automation throughout is significant. The February root cause involved an autonomous GitHub bot called hackerbot-claw that created an account on February 20 and systematically scanned repositories for exploitable GitHub Actions workflow patterns. Its profile described a vulnerability pattern index covering 9 classes and 47 sub-patterns. Whether hackerbot-claw was operated by TeamPCP or was a separate actor whose access TeamPCP subsequently exploited is not confirmed. Wiz explicitly notes the two incidents are distinct, and ARMO similarly frames the February AI agent attack as a separate event from the March 19 TeamPCP operation. The March deployment itself — 76 tags force-pushed in seconds, 64+ npm packages poisoned in under a minute, 44 repos defaced in under two minutes — is consistent with continued automation throughout the campaign.

LAPSUS$ affiliation

During the campaign, OpenSourceMalware researcher Paul McCarty flagged activity in the LAPSUS$ Telegram channel showing direct coordination with TeamPCP. A message in the channel read: "TeamPCP gonna do another large Supply chain attack, be ready for it," followed by a link to the TeamPCP channel.

A subsequent message referenced a "35k stars github repo," indicating awareness of a specific upcoming target before the attack became public.

This is the clearest public evidence of a relationship between TeamPCP and LAPSUS$ — a group previously associated with high-profile breaches at Okta, Microsoft, and Nvidia through social engineering. This connection was subsequently confirmed by Mandiant CTO Charles Carmakal at an RSA Conference briefing this week, where he stated the two groups were actively collaborating. If the coordination reflects an ongoing operational relationship rather than a one-off tip, TeamPCP has access to the intelligence-sharing and support structures of a larger criminal network. The practical implication is that future targets may be telegraphed in a closed channel before any public indicator appears.

Attack timeline

Phase

Target

Date

Access Vector

0

Trivy (Pwn Request)

Feb 27

AI agent exploits pull_request_target workflow; PAT exfiltrated

1

Trivy (full campaign)

Mar 19

Credentials retained from incomplete Feb rotation reused 20 days later

2

Aqua aquasec-com org

Mar 22

Argon-DevOps-Mgt service account token harvested from Trivy CI runner

3

npm / CanisterWorm

Mar 20–22

npm publish tokens harvested from Trivy CI runners

4

LiteLLM / BerriAI (PyPI)

Mar 24

PyPI token exfiltrated from LiteLLM's CI pipeline running compromised Trivy

5

Checkmarx Actions + OpenVSX

Mar 23–24

Reused stealer tradecraft; credentials from prior harvest

6

Telnyx

Mar 27

Same RSA key / stealer tradecraft as LiteLLM phase

Phase 0: A Pwn request against Trivy (February 27, 2026)

How access was obtained: An autonomous bot called hackerbot-claw opened PR #10252 against the Trivy repository. The PR exploited Trivy's "API Diff Check" workflow, which used a pull_request_target configuration — a known-dangerous pattern that runs with base-repository secrets while executing code from a fork. The workflow ran a curl-to-bash payload that used /proc/pid/mem memory dumping to extract the aqua-bot service account's Personal Access Token, a token with repo scope across the aquasecurity GitHub organization. The PAT was exfiltrated to recv.hackmoltrepeat[.]com. Using the stolen token, the attacker deleted all 178 Trivy GitHub releases (v0.27.0 through v0.69.1), briefly made the repository private, and published a malicious VS Code extension to OpenVSX. ARMO notes that Trivy was one of seven repositories targeted in this campaign, with five of seven successfully compromised. Aqua Security detected the breach and publicly disclosed it on March 1, rotating credentials at that time… but the rotation was not atomic. Valid tokens persisted through the rotation window, and the attacker retained access for 20 days.

What could have prevented it: Auditing CI/CD workflows for the pull_request_target pattern is the baseline control. This is a documented dangerous configuration with a long history of exploitation. Using short-lived OIDC tokens rather than long-lived PATs closes the residual-access window that non-atomic rotation creates. If a PAT is stolen and rotation is not immediate and simultaneous across all systems, the attacker retains a valid credential indefinitely.

Phase 1: Trivy poisoned (March 19, 2026)

How access was obtained: Using credentials retained through the incomplete February rotation, the attacker at 17:43 UTC used the aqua-bot service account to push a malicious v0.69.4 tag to Trivy and trigger the automated release pipeline. The attacker also pushed malicious workflows to tfsec and traceeshark, two other Aqua Security projects, and force-pushed 76 of 77 version tags in aquasecurity/trivy-action and all 7 tags in aquasecurity/setup-trivy to malicious commits. Additional Aqua credentials were stolen during this phase beyond standard CI secrets: GPG keys, Docker Hub credentials, Twitter credentials, and Slack credentials — exfiltrated to a separate Cloudflare Tunnel C2 at plug-tab-protective-relay.trycloudflare.com. Rami McCarthy, Principal Security Researcher at Wiz, documented this phase in detail.

The malicious binary was distributed across GitHub Releases, Docker Hub, GHCR, AWS ECR, deb/rpm repositories, and get.trivy.dev before removal approximately 3 hours later around 21:36 UTC. The compromised GitHub Actions tags remained active for approximately 12 hours. The imposter commits spoofed legitimate maintainer identities with backdated timestamps. Forensic tells were visible in retrospect: commits were unsigned where originals were GPG-signed, modified only entrypoint.sh where originals touched multiple files, and carried March 2026 parent commits despite claiming timestamps from 2021–2022. Tag force-pushes do not generate CreateEvent or DeleteEvent entries in GitHub's public events API, making them invisible to event-based monitoring.

The payload ran a collect-encrypt-exfiltrate loop on every CI runner that executed the action. On GitHub-hosted runners, it escalated to root via passwordless sudo, located Runner.Worker processes, and scraped /proc/pid/mem for secrets stored in the JSON pattern used by GitHub Actions to mask secrets. SSH keys, AWS/GCP/Azure credentials, Kubernetes tokens, Docker config, .env files, shell history, and cryptocurrency wallets were harvested, encrypted with AES-256-CBC under a hardcoded RSA-4096 public key, and exfiltrated as tpcp.tar.gz to scan.aquasecurtiy[.]org (note the typosquat). If that failed, the malware created a public repository named tpcp-docs in the victim's own GitHub account and uploaded the stolen data there as a release asset. On developer machines (detected by checking GITHUB_ACTIONS != "true"), the malware installed a persistent Python backdoor at ~/.config/systemd/user/sysmon.py polling for arbitrary payloads. Separately, malicious Docker Hub images 0.69.5 and 0.69.6 were pushed on March 22 without corresponding GitHub releases, confirming Docker Hub credentials had also been independently compromised.

What could have prevented it: Pinning GitHub Actions to full commit SHAs rather than mutable version tags is the single most impactful control and would have blocked this phase entirely. Verifying GPG signatures on security-critical workflow commits provides a detection signal when imposter commits appear. Monitoring CI/CD runners for unexpected egress to newly registered domains would surface the exfiltration traffic. Atomic, simultaneous credential revocation after the February breach would have closed the 20-day window between Phase 0 and Phase 1.

Phase 2: Aqua Security's internal GitHub org defaced (March 22, 2026)

How access was obtained: The Trivy CI pipeline also ran with a service account called Argon-DevOps-Mgt, a bot account with admin access bridging two GitHub organizations: the public aquasecurity org and the internal proprietary aquasec-com org. OpenSourceMalware's forensic analysis identified this account's token as the vector for the defacement based on the GitHub Events API. Seven hours before the defacement, the attacker tested the stolen token by creating and immediately deleting a branch named update-plugin-links-v0.218.2 on aquasecurity/trivy-plugin-aqua. No such release existed and no workflow was triggered, consistent with a stolen-token validation check. Seven hours later, all 44 repositories in aquasec-com were renamed with a tpcp-docs- prefix and defaced with "TeamPCP Owns Aqua Security" in under two minutes via automated API calls. Exposed repositories included source code for Tracee, internal Trivy forks, CI/CD pipelines, Kubernetes operators, and internal knowledge bases.

What could have prevented it: Service accounts should never hold long-lived PATs with cross-organization admin access. One compromised token became a single point of failure for both the public org and the internal one. Least-privilege scoping would have contained the blast radius significantly. Anomaly detection on service account behavior would have surfaced the ghost branch test as a detectable signal: creating and immediately deleting a branch referencing a non-existent release version is not consistent with legitimate operations.

Phase 3: CanisterWorm hits npm (March 20–22, 2026)

How access was obtained: The Trivy CI runner credential harvest included npm publish tokens belonging to maintainers whose packages ran Trivy in their pipelines. Aikido Security, which named and documented this attack, detected CanisterWorm on March 20 at 20:45 UTC. The worm used a three-stage architecture: a Node.js postinstall loader decoded a Python script, wrote it to ~/.local/share/pgmon/service.py, and installed it as a systemd user service named pgmon.service, masquerading as PostgreSQL monitoring. The worm's propagation logic read npm tokens from multiple locations including environment variables, called the npm /-/whoami endpoint to resolve each token's associated username, enumerated every package the account could publish to, bumped patch versions while preserving the original README, and published the malicious payload automatically. The initial burst compromised 28 packages in the @emilgroup scope in under 60 seconds. By the time Socket completed analysis, 141 malicious artifacts had been published across 66+ unique packages including @opengov, @teale.io, @airtm, and @pypestream scopes. The worm's C2 used ICP Canister node tdtqy-oyaaa-aaaae-af2dq-cai as a dead-drop for dynamic payload delivery, polling every 50 minutes. A youtube.com kill switch caused the backdoor to go dormant if the fetched URL contained that string.

What could have prevented it: npm publish tokens should be scoped to specific packages using granular automation tokens rather than full account credentials. CI scanning jobs and CI deployment jobs should use separate credentials: a job running Trivy for vulnerability scanning has no legitimate reason to hold npm publish tokens. Monitoring for anomalous package publication patterns would surface the automated burst; 64+ packages versioned and published in minutes is not a human behavior pattern.

Phase 4: LiteLLM PyPI H=hijack (March 23–24, 2026)

How access was obtained: BerriAI's CI/CD pipeline for LiteLLM ran Trivy as part of its build process, pulling it from apt without a pinned version during the Phase 1 exposure window. The compromised Trivy action exfiltrated the PYPI_PUBLISH token and GitHub PAT belonging to Krrish Dholakia (co-founder and CEO of LiteLLM / BerriAI) from the runner environment. The attacker used the PyPI token to publish malicious versions 1.82.7 at 10:39 UTC and 1.82.8 at 10:52 UTC, bypassing BerriAI's GitHub-based release pipeline entirely. No corresponding Git tags or commits exist in the LiteLLM source repository for either version. The packages were built locally and pushed using legitimate maintainer credentials, which meant they passed all standard cryptographic integrity checks including pip's hash verification — because the malicious content was correctly declared in the wheel's RECORD file with matching hashes. The initial discovery was not by a security researcher but by Callum McMahon, a research scientist at FutureSearch, whose development machine began dying with runaway processes and OOM errors after a Cursor MCP plugin pulled in LiteLLM as a transitive dependency. He had never intentionally installed it. PyPI quarantined the project at approximately 13:38 UTC, approximately three hours after the first malicious version appeared. BerriAI engaged Mandiant for incident response.

Version 1.82.7 embedded the stealer in proxy_server.py at line 128 between two legitimate code blocks, executing when anything imports litellm.proxy. Endor Labs identified three distinct payload iterations within proxy_server.py in this version alone, indicating real-time refinement during the attack window. Version 1.82.8, published 13 minutes later, added litellm_init.pth — a .pth file that Python's site module executes on every interpreter startup with no import required, including when pip, an IDE language server, or any Python subprocess starts. The 13-minute gap and escalating technique together indicate active iteration. Because LiteLLM typically sits between applications and multiple AI providers, a single compromised host could yield API keys for OpenAI, Anthropic, AWS Bedrock, and Google Vertex AI simultaneously. On Kubernetes, the payload attempted full cluster takeover by deploying privileged pods to every node and installing a persistence backdoor disguised as "System Telemetry Service."

What could have prevented it: PyPI Trusted Publishers (OIDC-based publishing) eliminates long-lived API tokens from CI pipelines entirely. If LiteLLM had used this mechanism, the stolen PYPI_PUBLISH token would have been worthless. Pinning Trivy to a specific version in the CI pipeline would have prevented the compromised action from running. Monitoring for .pth file creation in site-packages is a detection signal for this specific technique. One important note for any team relying on pip hash verification: that control did not protect against this attack because the malicious package was published with legitimate credentials and the RECORD file was correctly updated.

Phase 5: Checkmarx GitHub Actions and OpenVSX (March 23–24, 2026)

How access was obtained: TeamPCP deployed a nearly identical credential stealer in checkmarx/ast-github-action and checkmarx/kics-github-action. The payload collected CI runner secrets and exfiltrated them to checkmarx[.]zone using the same tpcp.tar.gz archive convention. If direct exfiltration failed, the malware created a docs-tpcp repository using the victim's own GITHUB_TOKEN and uploaded stolen data there as a release asset. Separately, malicious OpenVSX extensions were published through a compromised Checkmarx account: ast-results v2.53.0 and cx-dev-assist v1.7.0, which checked for cloud credentials and fetched a second-stage payload from checkmarx[.]zone. The official VS Code Marketplace was not affected.

Sysdig's Threat Research Team observed this phase in near-real time and documented a critical detection point: static analysis and domain reputation tools failed entirely because the malicious code was injected into trusted signed actions and the exfiltration domain was newly registered with a clean reputation score. Runtime detection of the underlying system calls succeeded where signature-based detection did not, because the attacker must ultimately execute system calls to steal and exfiltrate data regardless of delivery mechanism.

What could have prevented it: Pinning GitHub Actions to commit SHAs is the single control that would have blocked both Phase 1 and Phase 5. Egress filtering on CI runners would surface connections to newly registered domains. Monitoring for unexpected repository creation matching the tpcp-docs or docs-tpcp naming pattern provides a fallback detection signal when direct exfiltration is the primary path.

Phase 6: Telnyx (March 27, 2026)

How access was obtained: The Telnyx PyPI token was most likely harvested during the LiteLLM phase. Endor Labs researchers assessed that any developer or CI pipeline with both litellm installed and the telnyx PyPI token in its environment would have had that token swept during the Phase 4 credential harvest. Using it, the attacker published telnyx 4.87.1 and 4.87.2 to PyPI on March 27. The malicious code was injected into telnyx/_client.py, executing on any import of the package. PyPI quarantined the project shortly after discovery. Users should downgrade to 4.87.0 immediately.

The standout technique in this phase is the use of audio steganography for payload delivery. Rather than hosting a raw executable or encoded blob on the C2 (both trivially flagged by network inspection) the attacker embedded the final payload inside WAV audio files. On Windows, importing the package fetched hangup.wav from the C2 server at 83.142.209[.]203:8080, extracted an executable from the audio frames, and dropped it into the Startup folder as msbuild.exe for persistence across reboots. On Linux and macOS, the package fetched ringtone.wav instead, extracting a credential sweeper that ran a single high-speed collection pass, encrypted the results with AES-256-CBC under the same RSA-4096 public key used in the LiteLLM payloads (the strongest cross-phase attribution link in the campaign) and exfiltrated them as tpcp.tar.gz before deleting the temporary directory and leaving near-zero forensic artifacts. The Linux and macOS path included no persistence mechanism, a deliberate design choice: smash-and-grab rather than long-term access.

What could have prevented it: PyPI Trusted Publishers (OIDC-based publishing) eliminates long-lived API tokens from CI pipelines entirely, the same control that would have stopped the LiteLLM phase. Monitoring outbound connections from CI runners for requests to newly registered IPs (83.142.209[.]203 had no prior reputation) would have surfaced the exfiltration. For defenders assessing whether they were affected: audit Python environments for telnyx==4.87.1 or telnyx==4.87.2, check Windows Startup folders for msbuild.exe, and rotate all credentials accessible from any environment where the package was imported.

What makes this campaign unique?

No novel exploit was required at any point. The pull_request_target misconfiguration is documented. Mutable GitHub Action tags are a known risk. Long-lived cross-org service account PATs are a well-understood weakness. TeamPCP built automation to exploit all of it at speed.

Security tooling was the deliberate attack surface. Trivy is a vulnerability scanner. KICS is an infrastructure-as-code analyzer. Checkmarx Actions are security workflow tools. Compromising them is more valuable than compromising a random dependency because organizations grant security tooling implicit trust and elevated CI permissions. As Endor Labs noted, TeamPCP has exclusively targeted security-adjacent software: tools that have the broadest access to credentials and infrastructure by design.

Credential theft and supply chain attacks are now a single operation. Each compromised environment yielded credentials that unlocked the next target. The Snyk analysis of the LiteLLM compromise captured this precisely: standard integrity checks including hash verification provided no protection, because the attack used legitimate credentials rather than tampering with existing packages. The hash matched. The package was signed correctly. The only reliable detection path is behavioral, not cryptographic.

Iran-targeted wiper

Beyond credential theft, the kamikaze.sh payload evolved through multiple versions on March 22 via the ICP canister dead-drop. Rami McCarthy was the first to document the Iran-targeted wiper, tracking the payload evolution in real time on his research site (ramimac.me/trivy-teampcp) as TeamPCP iterated through six payload versions on March 22. Early versions focused on Kubernetes escape and host persistence. Later versions, documented in detail by Aikido Security researcher Charlie Eriksen, added SSH propagation, Docker API exploitation on port 2375, and in version 3.3, Python payloads embedded inside WAV audio files (bg_kube.wav, bg_prop.wav) using steganography to evade detection.

The geotargeted branch checked for Asia/Tehran timezone, Iran locale, or fa_IR locale indicators. On Kubernetes clusters matching those indicators, the payload deployed a privileged DaemonSet named host-provisioner-iran with a container named "kamikaze" across every node including the control plane, then executed a forced filesystem wipe and reboot. The non-Kubernetes Iranian path used rm -rf / --no-preserve-root if running as root, or attempted passwordless sudo first. Non-Iranian nodes received the CanisterWorm backdoor installed as pgmon.service for persistent C2 access. As Aikido noted, the Kubernetes DaemonSet path standardized on a find-based wipe command with forced reboot, while the non-Kubernetes path used the rm -rf approach.

The combination of global opportunistic credential theft alongside geotargeted destruction in Iran has not been fully explained publicly. It is consistent with either a dual-purpose operation combining financial and political objectives, or a destructive component commissioned alongside the credential theft campaign. The LAPSUS$ coordination adds context without resolving the question.

Key IOCs

Type

Indicator

C2 domain

scan.aquasecurtiy[.]org (typosquat, note misspelling) / 45.148.10.212

C2 domain

models.litellm.cloud (LiteLLM phase exfil)

C2 domain

checkmarx[.]zone (Checkmarx phase C2 and second-stage)

C2 domain

83.142.209[.]203:8080 (Telnyx phase C2 and exfil endpoint)

C2 fallback

tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io (ICP Canister)

C2 fallback

plug-tab-protective-relay.trycloudflare.com (Phase 1 additional exfil)

C2 fallback

Multiple *.trycloudflare.com tunnels rotated by phase

Archive name

tpcp.tar.gz (consistent across all phases)

GitHub pattern

tpcp-docs-* (defacement prefix on renamed repos)

GitHub pattern

tpcp-docs / docs-tpcp (fallback exfiltration repos created in victim orgs)

PyPI

litellm==1.82.7 and litellm==1.82.8 (quarantined)

PyPI

telnyx==4.87.1 and telnyx==4.87.2 (quarantined)

File

litellm_init.pth (SHA256: ceNa7wMJnNHy1kRnNCcwJaFjWX3pORLfMh7xGL8TUjg)

File

hangup.wav (Windows payload container, WAV steganography)

File

ringtone.wav (Linux/macOS payload container, WAV steganography)

OpenVSX

ast-results v2.53.0 / cx-dev-assist v1.7.0

File system

/tmp/pglog / /tmp/.pg_state / ~/.config/sysmon/sysmon.py

File system

~/.local/share/pgmon/service.py (CanisterWorm Python backdoor)

File system

%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exe (Windows persistence)

Systemd

pgmon.service / pgmonitor.service / internal-monitor.service

Kubernetes

host-provisioner-std / host-provisioner-iran (privileged DaemonSets in kube-system)

Commit

1885610c6a34811c8296416ae69f568002ef11ec (imposter Trivy commit, spoofing DmitriyLewen)

Commit

70379aad1a8b40919ce8b382d3cd7d0315cde1d0 (imposter actions/checkout commit, spoofing rauchg)

References