Part I covers the Pwn Request on Feb 27, the non-atomic rotation that kept the door open for 18 days, and the coordinated three-vector strike on March 19. This part picks up where that ends.
Where we left off
By end of day March 19, TeamPCP had force-pushed 82 GitHub Actions tags across trivy-action and setup-trivy, published a backdoored v0.69.4 binary across GitHub Releases, GHCR, Docker Hub, and ECR, and scraped Runner.Worker process memory across thousands of victim pipelines, recovering GITHUB_TOKEN, AWS_SECRET_ACCESS_KEY, PYPI_API_TOKEN, NPM_TOKEN, and everything else those pipelines had in scope.
The tokens recovered from those runners included credentials for npm package scopes, Checkmarx’s CI systems, and LiteLLM’s PyPI publisher account. What happened between March 19 and March 24 is the part of this campaign with the longest tail for the industry, specifically because of what LiteLLM is, not just what it does.
Researcher's note
The LiteLLM piece is what made me want to write this as a two-part series rather than a single incident summary. Most supply chain compromises have a bounded blast radius: one package, N organizations, one class of secrets. LiteLLM inverts that model entirely. It is not a target — it is a position. Compromise the proxy and you don't just get one organization's AI keys; you get every request, every response, every system prompt from every organization running that proxy in production, indefinitely, until the version is updated. That's a fundamentally different threat to account for, and I haven't seen it framed that way in any of the vendor advisories covering this campaign.
TL;DR
- CanisterWorm used stolen npm tokens to compromise 44+ packages across five scopes in under 60 seconds via automated
postinstallhook injection. C2 ran on ICP (Internet Computer Protocol), a decentralized compute platform; no domain to block. - LiteLLM’s PyPI publish token was recovered from a victim CI pipeline. TeamPCP used it to publish versions 1.82.7 and 1.82.8 on March 24. Both quarantined by PyPI at 11:25 UTC: a 46-minute exposure window at 95 million monthly downloads.
- The malicious code was never in LiteLLM’s GitHub repository. The injection happened post-build, in the wheel artifact, before upload.
pipsaw nothing wrong becausepipverifies internal consistency, not source fidelity. - 1.82.8 added
litellm_init.pth(34,628 bytes): a Python.pthfile that executes the full credential-harvesting payload on every Python invocation system-wide, with no import of LiteLLM required. - LiteLLM is not just another compromised package. It is the credentials vault for AI infrastructure. A compromised LiteLLM proxy running in production can log, modify, and redirect every AI interaction passing through it, indefinitely, without any payload visible in process memory.
- The complete IOC table, filesystem artifacts, and a six-check threat hunting script are at the end of this post.
March 20 — CanisterWorm: 44+ npm packages in under 60 seconds
The Runner.Worker scraper had recovered npm publish tokens with write access to several scoped package namespaces. TeamPCP deployed CanisterWorm (deploy.js): an automated tool that, given a valid npm publish token, enumerates every package in the token’s accessible scope via the registry API and publishes a new patch version of each with a credential-stealing postinstall hook injected. (ATT&CK: T1195.002 — Compromise Software Supply Chain)
Result in under 60 seconds: 28 packages under @EmilGroup, 16 under @opengov, and additional packages under @teale.io, @airtm, and @pypestream. Every developer or CI pipeline that ran npm install for an affected package executed the credential stealer at install time, no runtime import, no user action.
The postinstall script is the attack surface: npm runs it automatically after installing any package that defines it. Auditing hooks in your dependency tree before installing is the direct mitigation:
# Audit postinstall hooks in your project before installing
npm install --dry-run 2>&1 | grep -i "postinstall\|script"
# Check a specific package's scripts before installing
npm pack <package>@<version> --dry-run --json | jq '.[0].scripts'
CanisterWorm’s own C2 used an Internet Computer Protocol (ICP) canister: tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0[.]io. ICP is a decentralized compute platform where smart contracts run at network-level endpoints. Blocking a traditional C2 IP is straightforward; blocking *.icp0.io without disrupting legitimate ICP applications is not. The canister was denylisted on March 22.
Persistence on compromised developer machines: ~/.config/systemd/user/pgmon.service masquerading as “PostgreSQL Monitor”, staging payloads at /tmp/pglog, tracking state at /tmp/.pg_state.
March 21–22 — Checkmarx KICS and Docker Hub
Using credentials recovered from Trivy’s CI, TeamPCP published checkmarx/[email protected] with a malicious setup.sh on March 21 at 12:58 UTC (exposure window closed ~16:50 UTC). The payload architecture was identical to Trivy’s: AES-256-CBC + RSA-4096, tpcp.tar.gz, exfiltration to checkmarx[.]zone (typosquat of Checkmarx’s domain, resolving to 83.142.209.11:443).
Two new Kubernetes artifacts appeared in this wave: DaemonSets named host-provisioner-std and host-provisioner-iran deployed to kube-system. DaemonSets run on every node in the cluster and persist across pod restarts and namespace cleanups. Detecting them requires explicitly checking kube-system, which most security tooling does not scan by default. (ATT&CK: T1611 — Escape to Host)
On March 22, aquasec/trivy:0.69.5, 0.69.6, and latest were published to Docker Hub. Any organization pinning Trivy by tag in their container pipeline, not by digest, automatically pulled the malicious image on their next run. Same principle as GitHub Actions tag poisoning, different distribution channel.
March 24 — LiteLLM: the wheel that wasn’t in GitHub
Twenty-two hours after the KICS compromise, at approximately 10:39 UTC on March 24, litellm==1.82.7 was published to PyPI. Version 1.82.8 followed 13 minutes later. Both were quarantined by PyPI at 11:25 UTC.
46-minute exposure window. 95 million monthly downloads. 36% of cloud environments.
Why LiteLLM is a different class of target
LiteLLM is not just another Python package that happened to be compromised. It is an AI API gateway: a reverse proxy that sits between your application and every AI provider you use.
ANTHROPIC_API_KEY = sk-ant-...
AWS_BEDROCK credentials
AZURE_OPENAI_KEY
COHERE_API_KEY / MISTRAL_API_KEY
Every AI provider you use"] end LLM --> OAI["OpenAI"] LLM --> ANT["Anthropic"] LLM --> BED["AWS Bedrock"] LLM --> AZU["Azure / Cohere / Mistral..."] style LLM fill:#fff5f5,stroke:#c92a2a,stroke-width:2px
Every AI provider API key lives in LiteLLM’s environment, by design. That is the product’s value proposition: one proxy, standardized API, all providers. Compromising the package compromises all of them simultaneously.
But credential theft is only the beginning of what a compromised LiteLLM proxy running in production can do:
Standard PyPI package compromise:
— Steal credentials from the developer's machine or CI pipeline
Compromised LiteLLM proxy in production:
— All of the above, plus:
— Log every prompt sent to every AI provider (RAG content, system prompts, user data)
— Log every AI response received (proprietary model outputs, reasoning chains)
— Modify prompts before they reach the model (infrastructure-level prompt injection)
— Modify responses before they reach your application
— Redirect requests to attacker-controlled models
— All API keys aggregated in one process, one exfil payload
TeamPCP’s payload treated LiteLLM like any other package: standard credential harvester, in and out in 46 minutes. A more patient attacker running the same supply chain compromise could sit silently in the proxy layer and collect everything passing through it, indefinitely, without any payload detectable in process memory.
(OWASP LLM03:2025 — Supply Chain Vulnerabilities)
The post-build injection technique
The PyPI publish token was recovered from a victim CI pipeline that had LiteLLM as a dependency and happened to have the token in scope as an environment secret. TeamPCP did not need to compromise LiteLLM’s GitHub, their build pipeline, or their maintainer accounts. They needed one thing: the PyPI API token.
A Python wheel (.whl) is a ZIP archive. The workflow: download the legitimate 1.82.6 wheel, unzip it, inject malicious code into proxy_server.py, regenerate the RECORD file (which contains SHA-256 hashes of every file in the package) to match the modified content, rezip, publish as 1.82.7.
From pip’s perspective, the package is internally consistent; every file’s hash matches RECORD. pip does not compare distributed artifacts against source commits. That check does not happen automatically anywhere in the standard Python toolchain. (ATT&CK: T1195.002)
← 12 lines injected at line 128"] REC["dist-info/RECORD
← SHA-256 regenerated by attacker
← pip verification passes"] end subgraph wheel2 ["litellm-1.82.8 adds:"] PTH["litellm_init.pth (34,628 bytes)
← system-wide persistence on every Python invocation"] end GH["GitHub source at same tag
proxy_server.py — clean
No litellm_init.pth"] GH -->|diff reveals injection| wheel style PY fill:#fff5f5,stroke:#c92a2a style PTH fill:#fff5f5,stroke:#c92a2a style REC fill:#fff9f0,stroke:#e07c31
The only reliable detection path is comparing the wheel against the GitHub source at the same tag. The script below does it in 30 seconds and catches both the file injection and any unexpected .pth files:
#!/bin/bash
# verify-wheel-vs-source.sh
# pip, poetry, pipenv — none of them do this check automatically.
PACKAGE=$1 # e.g., litellm
VERSION=$2 # e.g., 1.82.6
GITHUB_REPO=$3 # e.g., https://github.com/BerryAI/litellm
WORKDIR=$(mktemp -d)
trap "rm -rf $WORKDIR" EXIT
pip download "${PACKAGE}==${VERSION}" --no-deps -d "$WORKDIR/wheel/" -q
unzip -q "$WORKDIR/wheel/"*.whl -d "$WORKDIR/extracted/"
git clone -q --depth 1 --branch "v${VERSION}" "$GITHUB_REPO" "$WORKDIR/source/" 2>/dev/null
echo "── ${PACKAGE}==${VERSION}: wheel vs source ──"
find "$WORKDIR/extracted/" -name "*.pth" -not -path "*/dist-info/*" \
| while read f; do
echo "⚠ Unexpected .pth: $(basename "$f")"
echo " $(head -c 200 "$f")"
done
DIFF=$(diff -rq \
"$WORKDIR/extracted/${PACKAGE}/" \
"$WORKDIR/source/${PACKAGE}/" \
--exclude="*.pyc" --exclude="__pycache__" 2>/dev/null)
[ -z "$DIFF" ] \
&& echo "✓ Wheel matches source" \
|| { echo "✗ DIVERGENCE — possible post-build injection"; echo "$DIFF" | head -40; }
Version 1.82.7 — the proxy_server.py injection
SHA-256: 8395c3268d5c5dbae1c7c6d4bb3c318c752ba4608cfcd90eb97ffb94a910eac2
Twelve lines injected at line 128 of litellm/proxy/proxy_server.py, placed between two unrelated legitimate code blocks to minimize visual disruption in a diff. The payload decodes via double base64 at runtime; the dangerous code is only visible after two decode rounds, which most static analysis tools do not apply to string literals.
The attacker’s development history is preserved in the binary artifact. Endor Labs documented three development iterations:
- Iteration 1: Custom RC4 cipher (abandoned; weak and detectable by entropy analysis)
- Iteration 2: Both old and new harvester versions coexisting (transition artifact)
- Iteration 3: RC4 removed; direct
exec()/eval()calls replaced by subprocess piping (harder to catch with static analysis rules that grep forexec()
C2 for 1.82.7: checkmarx[.]zone, reusing infrastructure from the KICS attack the previous day.
Version 1.82.8 — the .pth escalation (13 minutes later)
SHA-256: d2a0d5f564628773b6af7b9c11f6b86531a875bd2d186d7081ab62748a800ebb
SHA-256 of litellm_init.pth: 71e35aef03099cd1f2d6446734273025a163597de93912df321ef118bf135238
The 13-minute gap between 1.82.7 and 1.82.8 means TeamPCP was actively iterating during the attack window. They watched the first version land and pushed a more aggressive variant almost immediately.
The addition is litellm_init.pth. Most developers have never encountered Python .pth files. When Python starts (any process, invoked any way), the interpreter reads every .pth file in site-packages before executing anything else. Lines beginning with import are executed directly. When pip installs a wheel containing a .pth file, that file lands in site-packages and begins executing on every subsequent Python invocation system-wide, regardless of whether LiteLLM is imported, regardless of what the script does, regardless of whether the user knows LiteLLM is installed. (ATT&CK: T1546 — Event-triggered Execution)
Every one of these triggers the payload after 1.82.8 is installed:
python3 -c "print('hello')" python3 manage.py runserver
python3 -m pytest tests/ python3 -m pip install anything
uvicorn app:app celery worker --app=tasks
jupyter notebook python3 train.py
There is no way to use Python in an affected environment without triggering the payload, short of removing litellm_init.pth from site-packages first.
C2 upgraded to models[.]litellm[.]cloud, crafted to blend into legitimate LiteLLM traffic in proxy logs and network monitoring.
To verify this mechanism safely in an isolated environment:
python3 -m venv /tmp/pth-demo && source /tmp/pth-demo/bin/activate
SITE=$(python3 -c "import site; print(site.getsitepackages()[0])")
echo "import sys; open('/tmp/pth_demo_ran.txt', 'a').write('executed\n')" \
> "$SITE/demo_test.pth"
python3 -c "print('hello')" # triggers the .pth side-effect
python3 -m pip list --quiet # triggers it again
wc -l /tmp/pth_demo_ran.txt # outputs: 2
rm "$SITE/demo_test.pth" /tmp/pth_demo_ran.txt && deactivate
What the payload actually collected
The Stage 2 credential harvester (332 lines, documented by Endor Labs) swept comprehensively. The scope is worth itemizing because “CI/CD credentials” understates what 36% of cloud environments actually contain:
Identity: SSH private keys (id_rsa, id_ed25519, id_ecdsa, id_dsa), shell histories, /etc/passwd, /etc/shadow
Cloud providers: AWS via IMDS at 169.254.169.254 (role credentials, Secrets Manager, SSM parameters); GCP application_default_credentials.json; Azure ~/.azure/ directory (ATT&CK: T1552.005 — Cloud Instance Metadata API)
Kubernetes: In-pod service account tokens; full dump of all accessible namespace secrets via kubectl get secrets -A
Application secrets: .env files recursively across /home, /root, /opt, /srv, /var/www, /app, /data, /var/lib, /tmp (six directories deep); database configs (.pgpass, my.cnf, .mongorc.js); CI/CD config files (terraform.tfvars, terraform.tfstate, .gitlab-ci.yml, Jenkinsfile); TLS/SSL private keys (.pem, .key, .p12, .pfx)
Cryptocurrency: Bitcoin wallet.dat; Ethereum keystore files; Solana validator keypairs
Everything encrypted with AES-256-CBC (session key wrapped under embedded RSA-4096 public key, OAEP padding) and POSTed as tpcp.tar.gz to models[.]litellm[.]cloud. No plaintext on the wire.
Stage 3 persistence: ~/.config/sysmon/sysmon.py with systemd unit ~/.config/systemd/user/sysmon.service (“System Telemetry Service”), polling checkmarx[.]zone/raw every 50 minutes after a 5-minute initial delay. Kill switch: execution halts if youtube.com is unreachable (sandbox indicator). (ATT&CK: T1543.002 — Systemd Service)
For systems with Kubernetes access: privileged pods named node-setup-{node_name} in kube-system with hostPID: true, hostNetwork: true, privileged: true, full host filesystem mounted, and tolerations allowing scheduling on every node including control plane. Host-level execution across the entire cluster, surviving namespace cleanups.
Complete IOC list
| Indicator | Type | Stage |
|---|---|---|
recv.hackmoltrepeat[.]com | C2 domain | Feb 27 PAT exfil |
scan.aquasecurtiy[.]org | C2 domain (typosquat, extra ‘i’) | Trivy runner scraper |
45.148.10[.]212 | IP (TECHOFF SRV LIMITED, Amsterdam) | Trivy C2 |
tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0[.]io | ICP canister | npm CanisterWorm C2 |
plug-tab-protective-relay.trycloudflare[.]com | Cloudflare Tunnel | Payload rotation |
checkmarx[.]zone | C2 domain (typosquat) | KICS + LiteLLM 1.82.7 |
83.142.209[.]11:443 | IP | KICS C2 |
models[.]litellm[.]cloud | C2 domain | LiteLLM 1.82.8 |
litellm==1.82.7 | PyPI package | Post-build injection |
litellm==1.82.8 | PyPI package | Post-build injection + .pth |
8395c3268d5c5dbae1c7c6d4bb3c318c752ba4608cfcd90eb97ffb94a910eac2 | SHA-256 | litellm-1.82.7 wheel |
d2a0d5f564628773b6af7b9c11f6b86531a875bd2d186d7081ab62748a800ebb | SHA-256 | litellm-1.82.8 wheel |
a0d229be8efcb2f9135e2ad55ba275b76ddcfeb55fa4370e0a522a5bdee0120b | SHA-256 | proxy_server.py (injected) |
71e35aef03099cd1f2d6446734273025a163597de93912df321ef118bf135238 | SHA-256 | litellm_init.pth |
822dd269ec10459572dfaaefe163dae693c344249a0161953f0d5cdd110bd2a0 | SHA-256 | Trivy linux-amd64 v0.69.4 binary |
0880819ef821cff918960a39c1c1aada55a5593c61c608ea9215da858a86e349 | SHA-256 | Trivy windows-amd64 v0.69.4 binary |
6328a34b26a63423b555a61f89a6a0525a534e9c88584c815d937910f1ddd538 | SHA-256 | Trivy macOS-arm64 v0.69.4 binary |
checkmarx/[email protected] | GitHub Action | KICS entry point |
1885610c | Git commit (partial) | Spoofed DmitriyLewen |
8afa9b9f | Git commit (partial) | Spoofed Tomochika Hara |
node-setup-* pods in kube-system | Kubernetes | Stage 3 |
host-provisioner-std, host-provisioner-iran DaemonSets | Kubernetes | KICS Stage 3 |
~/.config/sysmon/sysmon.py | Filesystem | Persistence (all stages) |
~/.config/systemd/user/sysmon.service | Filesystem | Persistence (all stages) |
~/.config/systemd/user/pgmon.service | Filesystem | CanisterWorm |
~/.local/share/pgmon/service.py | Filesystem | CanisterWorm |
/tmp/pglog, /tmp/.pg_state | Filesystem | Payload staging |
tpcp-docs-* repos in victim GitHub accounts | GitHub | Exfiltration confirmed |
Threat hunting script
#!/bin/bash
# teampcp-hunt.sh — run on any machine that may have been exposed
ISSUES=0
echo "╔══════════════════════════════════════════╗"
echo "║ TeamPCP Threat Hunt ║"
echo "╚══════════════════════════════════════════╝"
# 1. Filesystem persistence artifacts
echo ""
echo "[1/6] Persistence files..."
for f in \
"$HOME/.config/sysmon/sysmon.py" \
"$HOME/.config/systemd/user/sysmon.service" \
"$HOME/.config/systemd/user/pgmon.service" \
"$HOME/.local/share/pgmon/service.py" \
"/tmp/pglog" "/tmp/.pg_state"
do
if [ -f "$f" ]; then
echo " ✗ FOUND: $f [sha256: $(sha256sum "$f" | cut -d' ' -f1)]"
ISSUES=$((ISSUES+1))
fi
done
[ $ISSUES -eq 0 ] && echo " ✓ None found"
# 2. Suspicious .pth files
echo ""
echo "[2/6] Python site-packages .pth files..."
PTH_ISSUES=0
while IFS= read -r site_dir; do
for pth in "$site_dir"/*.pth; do
[ -f "$pth" ] || continue
if grep -qE "(exec|subprocess|urllib|base64|eval|__import__)" "$pth" 2>/dev/null; then
echo " ✗ SUSPICIOUS: $pth"
echo " $(head -c 160 "$pth")"
PTH_ISSUES=$((PTH_ISSUES+1))
fi
done
done < <(python3 -c "import site; print('\n'.join(site.getsitepackages()))" 2>/dev/null)
[ $PTH_ISSUES -eq 0 ] && echo " ✓ No suspicious .pth files"
ISSUES=$((ISSUES+PTH_ISSUES))
# 3. Compromised LiteLLM version
echo ""
echo "[3/6] LiteLLM version check..."
LITELLM_VER=$(python3 -c "import litellm; print(litellm.__version__)" 2>/dev/null || echo "not installed")
if echo "$LITELLM_VER" | grep -qE "^1\.82\.[78]$"; then
echo " ✗ COMPROMISED: litellm==$LITELLM_VER — rotate all AI provider API keys immediately"
ISSUES=$((ISSUES+1))
else
echo " ✓ LiteLLM: $LITELLM_VER"
fi
# 4. Systemd backdoor
echo ""
echo "[4/6] Systemd sysmon service..."
if systemctl --user is-active sysmon &>/dev/null 2>&1; then
echo " ✗ ACTIVE: sysmon.service is running"
ISSUES=$((ISSUES+1))
elif systemctl --user is-enabled sysmon &>/dev/null 2>&1; then
echo " ✗ ENABLED: sysmon.service exists (not currently running)"
ISSUES=$((ISSUES+1))
else
echo " ✓ No sysmon service"
fi
# 5. Kubernetes
echo ""
echo "[5/6] Kubernetes..."
if command -v kubectl &>/dev/null && kubectl cluster-info &>/dev/null 2>&1; then
K8S_HITS=$(kubectl get pods,daemonsets -A --no-headers 2>/dev/null \
| grep -iE "node-setup-|host-provisioner|sysmon" || true)
if [ -n "$K8S_HITS" ]; then
echo " ✗ SUSPICIOUS resources:"
echo "$K8S_HITS" | sed 's/^/ /'
ISSUES=$((ISSUES+1))
else
echo " ✓ No suspicious pods or DaemonSets"
fi
else
echo " – kubectl not available"
fi
# 6. GitHub tpcp-docs repositories
echo ""
echo "[6/6] GitHub exfiltration indicator..."
if command -v gh &>/dev/null && gh auth status &>/dev/null 2>&1; then
TPCP=$(gh repo list --json name --limit 200 2>/dev/null \
| python3 -c "import sys,json; [print(r['name']) for r in json.load(sys.stdin) if 'tpcp-docs' in r['name']]" || true)
if [ -n "$TPCP" ]; then
echo " ✗ tpcp-docs repo found — exfiltration confirmed: $TPCP"
ISSUES=$((ISSUES+1))
else
echo " ✓ No tpcp-docs repositories"
fi
else
echo " – gh CLI not available"
fi
echo ""
echo "══════════════════════════════════════════"
if [ $ISSUES -gt 0 ]; then
echo " ✗ $ISSUES indicator(s) — treat as compromised, rotate credentials"
else
echo " ✓ No TeamPCP indicators found"
fi
echo "══════════════════════════════════════════"
What to rotate — and when
Absence of confirmed exfiltration evidence is not evidence of absence. The payload operated silently in background threads with encrypted output. Rotate first, investigate in parallel.
| Component | Exposure window | Credentials at risk |
|---|---|---|
trivy-action tags | Mar 19 17:43 – Mar 20 05:40 UTC | All runner secrets |
setup-trivy tags | Mar 19 17:43 – Mar 20 05:40 UTC | All runner secrets |
| Trivy binary v0.69.4 | Mar 19 17:43 – ~21:00 UTC | All runner secrets |
| Docker Hub v0.69.5/6 | Mar 22, ~8hr window | All runner secrets |
| KICS [email protected] | Mar 21 12:58 – 16:50 UTC | All runner secrets |
| LiteLLM 1.82.7 | Mar 24 10:39 – 11:25 UTC | All credentials + all AI provider API keys |
| LiteLLM 1.82.8 | Mar 24 10:52 – 11:25 UTC | All credentials + AI keys + system-wide Python |
Rotate immediately: GitHub tokens, AWS/GCP/Azure credentials, PyPI + npm + Docker Hub publish tokens, all AI provider API keys (OpenAI, Anthropic, AWS Bedrock, Azure OpenAI, Cohere, Mistral, Vertex AI)
Within 24 hours: SSH keys on CI runners, Kubernetes service account tokens, database credentials in .env files on runners
The broader implication for AI infrastructure
TeamPCP’s payload treated LiteLLM like any other package. But the proxy position LiteLLM occupies means a more patient attacker running the same compromise could collect ongoing intelligence (every prompt, every response, every system prompt, every RAG document) without any payload visible in memory, indefinitely, until the package version is updated.
Prompt injection has dominated AI security conversations because it is a model-level threat affecting every deployment. Proxy-layer supply chain compromise achieves everything prompt injection does, plus persistent credential access, plus bidirectional traffic manipulation, before any model-level defense acts, and across every organization using the proxy simultaneously.
The supply chain is the security perimeter for AI systems. TeamPCP just demonstrated what the first move looks like.
MITRE ATT&CK reference (click to expand)
| ID | Technique | Where |
|---|---|---|
| T1195.002 | Compromise Software Supply Chain | Wheel injection; npm postinstall; tag poisoning |
| T1528 | Steal Application Access Token | Runner.Worker memory scraping |
| T1546 | Event-triggered Execution | .pth file in site-packages |
| T1543.002 | Systemd Service | sysmon.service; pgmon.service |
| T1552.005 | Cloud Instance Metadata API | AWS IMDS at 169.254.169.254 |
| T1611 | Escape to Host | Kubernetes privileged pods + hostPID |
| T1036.005 | Match Legitimate Name | sysmon.py; pgmon.service masquerade |
| T1041 | Exfiltration Over C2 Channel | tpcp.tar.gz AES+RSA to C2 |
| T1567.001 | Exfiltration to Code Repository | tpcp-docs GitHub fallback |
| T1102 | Web Service | ICP canister (decentralized C2) |
References
- Endor Labs — TeamPCP Isn’t Done (Mar 24, 2026)
- Wiz Research — TeamPCP Trojanizes LiteLLM (Mar 24, 2026)
- Wiz Research — Trivy Compromised by TeamPCP (Mar 19, 2026)
- safedep.io — Trivy TeamPCP Supply Chain Compromise
- Microsoft Security Blog — Detecting the Trivy Supply Chain Compromise (Mar 25, 2026)
- Sysdig TRT — TeamPCP Expands to Checkmarx
- BleepingComputer — LiteLLM PyPI Package Backdoored (Mar 24, 2026)
- Palo Alto Unit 42 — When Security Scanners Become the Weapon
- Arctic Wolf — TeamPCP Supply Chain Attack Campaign
- OWASP — LLM Top 10 for Large Language Models 2025
- PyPI Advisory — PYSEC-2026-2: litellm 1.82.7, 1.82.8