pipebreach logo pipebreach.com
Incident Analysis

TeamPCP Part II: Backdooring the AI Credentials Vault

Critical npm PyPI openvsix supply-chain-compromise Analysis only
April 4, 2026 · 22 min read
LiteLLM as AI credentials vault — one compromised proxy, every AI provider exposed
← Part I: Twenty Days of Silent Access From a Two-Minute PR · Part II

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 postinstall hook 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. pip saw nothing wrong because pip verifies internal consistency, not source fidelity.
  • 1.82.8 added litellm_init.pth (34,628 bytes): a Python .pth file 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.

graph TD APP["Your application"] --> LLM subgraph LLM ["LiteLLM Proxy — what TeamPCP targeted"] direction TB K["OPENAI_API_KEY = sk-proj-...
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)

graph TD subgraph wheel ["litellm-1.82.7-py3-none-any.whl (ZIP archive)"] PY["proxy/proxy_server.py
← 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 for exec()

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)

sequenceDiagram participant U as Any Python invocation participant PY as Python interpreter participant SP as site-packages/ participant P as litellm_init.pth participant C2 as models[.]litellm[.]cloud U->>PY: python3 script.py (any script) PY->>SP: reads all .pth files on startup SP->>P: litellm_init.pth found P->>C2: decode payload, execute in subprocess Note over P,C2: fires before any user code runs PY->>U: script.py executes normally

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

IndicatorTypeStage
recv.hackmoltrepeat[.]comC2 domainFeb 27 PAT exfil
scan.aquasecurtiy[.]orgC2 domain (typosquat, extra ‘i’)Trivy runner scraper
45.148.10[.]212IP (TECHOFF SRV LIMITED, Amsterdam)Trivy C2
tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0[.]ioICP canisternpm CanisterWorm C2
plug-tab-protective-relay.trycloudflare[.]comCloudflare TunnelPayload rotation
checkmarx[.]zoneC2 domain (typosquat)KICS + LiteLLM 1.82.7
83.142.209[.]11:443IPKICS C2
models[.]litellm[.]cloudC2 domainLiteLLM 1.82.8
litellm==1.82.7PyPI packagePost-build injection
litellm==1.82.8PyPI packagePost-build injection + .pth
8395c3268d5c5dbae1c7c6d4bb3c318c752ba4608cfcd90eb97ffb94a910eac2SHA-256litellm-1.82.7 wheel
d2a0d5f564628773b6af7b9c11f6b86531a875bd2d186d7081ab62748a800ebbSHA-256litellm-1.82.8 wheel
a0d229be8efcb2f9135e2ad55ba275b76ddcfeb55fa4370e0a522a5bdee0120bSHA-256proxy_server.py (injected)
71e35aef03099cd1f2d6446734273025a163597de93912df321ef118bf135238SHA-256litellm_init.pth
822dd269ec10459572dfaaefe163dae693c344249a0161953f0d5cdd110bd2a0SHA-256Trivy linux-amd64 v0.69.4 binary
0880819ef821cff918960a39c1c1aada55a5593c61c608ea9215da858a86e349SHA-256Trivy windows-amd64 v0.69.4 binary
6328a34b26a63423b555a61f89a6a0525a534e9c88584c815d937910f1ddd538SHA-256Trivy macOS-arm64 v0.69.4 binary
checkmarx/[email protected]GitHub ActionKICS entry point
1885610cGit commit (partial)Spoofed DmitriyLewen
8afa9b9fGit commit (partial)Spoofed Tomochika Hara
node-setup-* pods in kube-systemKubernetesStage 3
host-provisioner-std, host-provisioner-iran DaemonSetsKubernetesKICS Stage 3
~/.config/sysmon/sysmon.pyFilesystemPersistence (all stages)
~/.config/systemd/user/sysmon.serviceFilesystemPersistence (all stages)
~/.config/systemd/user/pgmon.serviceFilesystemCanisterWorm
~/.local/share/pgmon/service.pyFilesystemCanisterWorm
/tmp/pglog, /tmp/.pg_stateFilesystemPayload staging
tpcp-docs-* repos in victim GitHub accountsGitHubExfiltration 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.

ComponentExposure windowCredentials at risk
trivy-action tagsMar 19 17:43 – Mar 20 05:40 UTCAll runner secrets
setup-trivy tagsMar 19 17:43 – Mar 20 05:40 UTCAll runner secrets
Trivy binary v0.69.4Mar 19 17:43 – ~21:00 UTCAll runner secrets
Docker Hub v0.69.5/6Mar 22, ~8hr windowAll runner secrets
KICS [email protected]Mar 21 12:58 – 16:50 UTCAll runner secrets
LiteLLM 1.82.7Mar 24 10:39 – 11:25 UTCAll credentials + all AI provider API keys
LiteLLM 1.82.8Mar 24 10:52 – 11:25 UTCAll 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


Action required Three things to do this week if LiteLLM runs in your stack

If LiteLLM 1.82.7 or 1.82.8 ran anywhere in your stack — do this first, before reading further:

# Check what versions have been on your systems
pip show litellm | grep Version
docker images --format "{{.Repository}}:{{.Tag}}" | grep litellm
kubectl get pods -A -o jsonpath='{range .items[*]}{.spec.containers[*].image}{"\n"}{end}' | grep litellm

If any result shows 1.82.7 or 1.82.8: rotate every AI provider key in your environment right now. OpenAI, Anthropic, Bedrock, Azure OpenAI, Cohere — all of them. The payload exfiltrated the full process environment. Rotating after confirming exfiltration is too late.


1. Verify your AI and security tooling wheels against their GitHub source.

The key insight: pip install verifies internal wheel consistency — it does not verify that the wheel matches what the maintainer published from source. This check does:

./verify-wheel-vs-source.sh litellm 1.82.6 https://github.com/BerryAI/litellm

Run this for LiteLLM first, then your vulnerability scanner, then any other Python security tooling in CI. The script above completes in 30 seconds per package. Differences in RECORD SHA-256 entries that don’t correspond to any commit in the source repo = injection.

2. Scan every site-packages directory for unexpected .pth files — right now.

.pth persistence is invisible to most EDR tooling because it’s not a process, not a cron job, and not a systemd unit. It runs before your application code, before logging initializes, on every Python invocation:

# Find all .pth files across all Python environments on this machine
python3 -c "import site; print('\n'.join(site.getsitepackages() + [site.getusersitepackages()]))" \
  | xargs -I{} find {} -name "*.pth" -exec grep -lE 'exec|subprocess|base64|urllib|__import__' {} \;

Anything that matches that grep in a .pth file is a red flag. Legitimate .pth files contain only directory paths, one per line.

3. If you run LiteLLM as a proxy in production — change your operational model.

LiteLLM’s proxy position means a compromised version doesn’t need to exfiltrate anything. It can modify requests and responses in transit, indefinitely, without leaving artifacts detectable in process memory after the fact. If a future version is compromised and you don’t catch it within minutes of deployment, you won’t have a clean “this is when exfiltration stopped” line. Treat the proxy tier the way you treat network infrastructure: pin versions, verify signatures, and monitor for any update that doesn’t come through your controlled deployment process.


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.


← Part I: Twenty Days of Silent Access From a Two-Minute PR

MITRE ATT&CK reference (click to expand)
IDTechniqueWhere
T1195.002Compromise Software Supply ChainWheel injection; npm postinstall; tag poisoning
T1528Steal Application Access TokenRunner.Worker memory scraping
T1546Event-triggered Execution.pth file in site-packages
T1543.002Systemd Servicesysmon.service; pgmon.service
T1552.005Cloud Instance Metadata APIAWS IMDS at 169.254.169.254
T1611Escape to HostKubernetes privileged pods + hostPID
T1036.005Match Legitimate Namesysmon.py; pgmon.service masquerade
T1041Exfiltration Over C2 Channeltpcp.tar.gz AES+RSA to C2
T1567.001Exfiltration to Code Repositorytpcp-docs GitHub fallback
T1102Web ServiceICP canister (decentralized C2)

References

  1. Endor Labs — TeamPCP Isn’t Done (Mar 24, 2026)
  2. Wiz Research — TeamPCP Trojanizes LiteLLM (Mar 24, 2026)
  3. Wiz Research — Trivy Compromised by TeamPCP (Mar 19, 2026)
  4. safedep.io — Trivy TeamPCP Supply Chain Compromise
  5. Microsoft Security Blog — Detecting the Trivy Supply Chain Compromise (Mar 25, 2026)
  6. Sysdig TRT — TeamPCP Expands to Checkmarx
  7. BleepingComputer — LiteLLM PyPI Package Backdoored (Mar 24, 2026)
  8. Palo Alto Unit 42 — When Security Scanners Become the Weapon
  9. Arctic Wolf — TeamPCP Supply Chain Attack Campaign
  10. OWASP — LLM Top 10 for Large Language Models 2025
  11. PyPI Advisory — PYSEC-2026-2: litellm 1.82.7, 1.82.8
DM
Daniel Malvaceda

Security Researcher

Security researcher focused on supply chain security, CI/CD attack surfaces, and AI security.