PyPI 上遭入侵的 LiteLLM 1.107.0 安装凭证窃取器和后门
Compromised LiteLLM 1.107.0 on PyPI Installs a Credential Stealer and Backdoor

原始链接: https://safedep.io/litellm-pypi-compromise-credential-stealer-backdoor/

PyPI 上的 LiteLLM 1.107.0 是一个被篡改的发布版本,包含凭证窃取器和远程代码执行后门。全部七个 wheel 包都安装了三个文件——cook.pth、cook.py 和 sitecustomize.py——到 site-packages 中,Python 在启动时执行其中两个,影响任何 Python 进程。上游 GitHub 源码中没有这些文件,PyPI 后来移除了该发布版本;1.106.2 版本和 rc 版本是干净的。 恶意的源码分发包在 maturin 中添加了 `data = "cook"`,将 cook/ 文件夹复制到 wheel 包中。sdist 其余部分与一个后续提交匹配,仅做了版本和依赖编辑。wheel 包看起来由 BerriAI 的正常流水线构建,但没有公开的 GitHub Actions 运行创建它们。 cook.pth 导入 cook.py,窃取 SSH、云、CI/CD、AI、浏览器和 LiteLLM 凭证,加密后发送到 www.litellm.dev。sitecustomize.py 运行一个分离的每小时守护进程,重新执行窃取器并运行从攻击者控制的服务器下载的代码。攻击者的 GitHub 账户后来被删除,仅先前受感染的主机仍处于风险中。

Hacker News 讨论了 PyPI 上 LiteLLM 1.107.0 被攻陷的问题,据报道该版本安装了凭证窃取器和后门。恶意 wheel 文件似乎来自 BerriAI 私有发布流水线中被攻击者修改的源码,可能使用了在日志中捕获的被泄露维护者/GitHub OAuth 令牌。用户被建议固定到被攻陷前的版本。LiteLLM 维护者表示事件已得到控制,并描述了涉及 Claude Code 令牌泄露的多步攻击。评论者讨论了防御措施:沙箱、通过代理注入凭证、依赖冷却、版本固定,以及在先前事件后放弃 LiteLLM。一些人建议使用基于 Go 的代理或自定义实现,而另一些人强调没有任何单一工具能消除供应链风险。讨论还包含对认证疲劳、开源安全和包管理器默认设置的更广泛抱怨。总体而言,该事件重新引发了人们对依赖信任和发布流水线安全的担忧。
相关文章

原文

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. PyPI has since removed the release.

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)

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

TEXT 4 lines

The release does not match its tag

The tag v1.107.0 points to commit 66c8f5b (06:59 UTC). Its pyproject.toml says version = "1.108.0". SafeDep compared every sdist file with the git trees by blob hash. The sdist matches a later main commit, a8113b0 (08:50 UTC), plus five edited files and the cook/ folder.

Four of the edited files are Cargo.toml files that gain a readme = "README.md" line. The legitimate 1.106.2 and rc2 releases have the same edit. The fifth is pyproject.toml, compared here with a8113b0:

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

DIFF 11 lines

The edit sets the version back to 1.107.0 and pins the companion packages to the rc2 values. As a result, a dependency diff against rc2 shows nothing unusual. The legitimate 1.106.2 sdist matches its tag commit 49943cd except for the Cargo.toml readme lines.

How the wheels were built

The malicious wheels match the output of the normal BerriAI release pipeline:

  • The WHEEL file names maturin 1.9.4, the same version as in the legitimate releases. The implant files have correct RECORD entries, which shows no edits after the build.
  • Zip timestamps show separate native builds: macOS at 11:20, Linux at 11:21, and Windows at 11:37 UTC.
  • In the Windows wheel, all 2,804 .py files have CRLF line endings, including the three implant files. The Linux wheel has none. A git checkout on Windows with line-ending conversion produces this pattern.

No public GitHub Actions run built or published the release. SafeDep checked all 539 runs between 09:30 and 12:30 UTC. At 11:50 UTC, about 10 minutes after the PyPI upload, litellm-dispatcher[bot] published the GitHub release v1.107.0. The same delay occurred for 1.106.2. PyPI has no provenance attestations for 1.107.0, 1.106.2, or rc2.

SafeDep assesses, with moderate to high confidence, that BerriAI’s private release pipeline built these wheels from an attacker-modified git source. The attacker could have used a compromised maintainer or bot credential, a private branch or mirror, or the reference the pipeline builds from. A stolen PyPI token used from the attacker’s own machines fits the evidence less well. Only BerriAI’s pipeline logs can confirm the cause.

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:

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

  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.

  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.

TimeEvent
06:59Tag v1.107.0 commit 66c8f5b created
08:50Commit a8113b0 on main, the base of the release source
10:44Legitimate 1.106.2 uploaded to PyPI
11:20 to 11:37Malicious 1.107.0 wheels built
11:39 to 11:401.107.0 uploaded to PyPI (all 8 files)
11:50litellm-dispatcher[bot] publishes GitHub release v1.107.0
13:40Observed: 1.107.0 gone from PyPI, letters9813 account deleted

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

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, from the wheel RECORD:

FileSHA-256 (URL-safe base64)
cook.pthfxkWMucyBwXRVLhSBPGuVGtUOsvX6IYSKbdvUEK5K9s
cook.pyndCjkm-Wo364bPHuIxDZvpMTnMTr7z754u_pUxOQviU
sitecustomize.pyNL7wT-iKydCq4EdWMLFzhoHRW5TyM6yKpG6s9ZaDBfo
联系我们 contact @ memedata.com