LiteLLM 1.107.0 版本在 PyPI 上发布时,被植入了凭证窃取器和远程代码执行后门。该恶意版本覆盖了 Linux、macOS 和 Windows 平台,所有七个轮子(wheels)均包含三个植入文件。这些文件会在主机上的任何 Python 进程启动时自动运行。值得注意的是,上游 GitHub 源代码中并不存在这些恶意文件。目前,PyPI 已删除了该版本。

经核查,版本 1.106.2、1.107.0rc1 和 1.107.0rc2 是干净的。其依赖的伴侣包 litellm-proxy-extras==0.4.110 和 litellm-enterprise==0.1.78 同样未被篡改。
攻击机制分析
安装 litellm==1.107.0 会在 site-packages 目录中放置 3 个文件。Python 在每次启动时都会加载其中的两个:
- cook.pth:导入
cook.py,这是一个身份窃取器。它负责收集 SSH 密钥、云凭证、CI/CD 秘密、AI 工具令牌、浏览器密码以及存储在 LiteLLM 代理数据库中的 LLM 提供商密钥。数据经过加密后发送至www.litellm[.]dev。 - sitecustomize.py:启动一个后台守护进程。该进程每小时从 GitHub 文件中读取服务器地址,以维持远程控制通道。
恶意代码如何进入发行版
在 1.107.0 的源分布(sdist)中,pyproject.toml 文件的构建部分增加了一行配置:
[tool.maturin]
data-dir = "cook"
设置 maturin data-dir="cook" 会将 cook/ 目录下的文件复制到 wheel 包的 litellm-1.107.0.data/purelib/ 中。pip 在安装时会将这些文件部署到 site-packages。
这是唯一可见的配置变更。在 BerriAI/litellm 仓库中,没有任何公开的 commit 包含 data-dir = "cook" 或 cook/ 文件。
Wheel 包的 RECORD 文件列出了三个新增文件:
../../../site-packages/cook.pth,sha256=...
../../../site-packages/cook.py,sha256=...
../../../site-packages/sitecustomize.py,sha256=...
发行版与标签不匹配
标签 v1.107.0 指向提交 66c8f5b (UTC 06:59)。然而,该提交的 pyproject.toml 显示版本号为 "1.108.0"。SafeDep 将每个 sdist 文件与 Git 树进行了比对,发现恶意 sdist 实际上匹配的是后续的主要提交 a8113b0 (UTC 08:50),外加五个编辑过的文件和 cook/ 目录。
其中四个编辑过的文件是 Cargo.toml,增加了 readme="README.md" 行。合法的 1.106.2 和 rc2 版本也有相同的编辑。第五个编辑过的文件是 pyproject.toml,与 a8113b0 相比:
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"
[project]
-name = "litellm"
-version = "1.108.0"
+name = "litellm"
+version = "1.107.0"
此编辑将版本号改为 1.107.0,并将伴侣包固定为 rc2 值。因此,针对 rc2 的依赖性差异看起来并无异常。合法的 1.106.2 sdist 与其标签提交 49943cd 完全匹配,除了 Cargo.toml 中的 readme 行。
Wheel 包的构建痕迹
恶意 Wheel 包与正常的 BerriAI 发布管道输出特征一致:
WHEEL文件名由 maturin 1.9.4 生成,与合法版本相同。植入文件拥有正确的 RECORD 条目,未显示构建后的编辑痕迹。- Zip 时间戳显示独立的原生构建过程:macOS 在 UTC 11:20,Linux 在 11:21,Windows 在 11:37。
- 在 Windows wheel 包中,全部 2,804 个 .py 文件均采用 CRLF 行尾格式,包括三个植入文件。Linux wheel 包则无此现象。这种模式通常出现在启用了行尾转换的 Git checkout 环境中。
没有公开的 GitHub Actions 运行记录用于构建或发布该版本。安全团队检查了 UTC 9:30 至 12:30 之间的所有 539 次运行,均未发现相关活动。在 PyPI 上传后约 10 分钟(UTC 11:50),litellm-dispatcher[bot] 发布了 GitHub 版本 v1.107.0。同样的延迟也发生在 1.106.2 版本上。PyPI 上没有 1.107.0、1.106.2 或 rc2 的来源证明。
据 SafeDep 估计,BerriAI 的私有发布管道是从攻击者修改后的 Git 源中构建了这些 Wheel 包。攻击者可能使用了受损的维护者或机器人凭证、私有分支或镜像。使用窃取的 PyPI 令牌从攻击者自己的机器直接上传不太符合现有证据。具体原因需通过 BerriAI 的内部管道日志才能证实。
负载 1:cook.py 中的凭证窃取器
Python 在启动时会执行 site-packages 中每个以 import 开头的 .pth 文件。cook.pth 包含 import cook,导致窃取器在主机上的每个 Python 进程中运行,包括应用程序、pip、IDE 语言服务器和 CI 作业。
cook.py 将大约 61KB 的 Python 代码包裹在 exec(zlib.decompress(base64.b64decode('...'))) 层中。解码后的代码以如下配置开头:
# Decoded configuration snippet
CONFIG = {
"endpoint": "https://www.litellm.dev/healthcheck",
"token": "XWiA8gZNa7NcX2XRoAEb"
}
收集的数据范围
| 模块 | 目标数据 |
|---|---|
| 网络信息 | 主机名、用户、操作系统、IP 地址、网络设置 |
| 浏览器_pw | Chromium 保存的密码(使用个人资料主密钥解密,Windows 上使用 DPAPI)、浏览器 logins.json 和 key4.db |
| 浏览器历史 | 浏览器历史记录 |
| ssh_keys | ~/.ssh/* 目录下的所有文件 |
| 终端记录 | bash 和 zsh 历史记录、PowerShell PSReadLine、cmd 历史记录 |
| 云_creds | ~/.aws/credentials 和 config、Azure 配置文件和令牌、gcloud 凭证、~/.kube/config、~/.docker/config.json、gh hosts.yml、rclone.conf、~/.netrc |
| 开发秘密 | $HOME 下的 .pem, .key, .pfx, .p12, .pk8, .ppk 文件、Composer auth.json、FileZilla 保存的服务器信息 |
| 容器_信贷 | Kubernetes 服务账户令牌 |
| DevOps 秘密 | GitLab Runner config.toml、Jenkins 初始管理员密码和 master.key、.vault-token、Terraform credentials.tfrc.json、Wrapper 设置 |
| AI 机器人 | Claude Code, Claude Desktop, Cursor, Windsurf, Copilot, VS Code mcp.json, Gemini CLI, Codex auth.json, Continue, Aider, Goose, Open Source, Qwen, Smash, Kiro, Zed, Hugging Face 令牌 |
| 在线服务 | 自身环境及通过 /proc/<pid>/environ 获取的其他进程的环境变量 |
| db_litellm | 读取 LiteLLM_CredentialsTable 和 LiteLLM_ProxyModelTable 表,其中包含 LiteLLM 代理的 LLM 提供商 API 密钥 |
| 云_元数据 | AWS 实例元数据服务 (IMDSv2, 然后 v1):实例身份和角色凭证 |
| GPG/TLS 密钥 | GPG 密钥、TLS 私钥 |
数据传输方式
- 使用随机 AES-256-CBC 密钥加密 JSON 结果,并使用 RSA-OAEP 和嵌入式公钥加密该 AES 密钥。
- 将结果打包成 tar.gz (hdr.json, data.bin, key.bin),并以 POST 请求发送到
www.litellm[.]dev/healthcheck,标题中包含X-Token:XWiA8gZNa7NcX2XRoAEb。 - 成功上传后,写出标记文件
/tmp/.pc_once_72129118c59f5636c0bb。同时设置环境变量PRIVCOLLECT_ACTIVE=1,以避免在同一进程树中重复运行。
负载 2:sitecustomize.py 中的后门
Python 在启动时会导入 sitecustomize,因此该文件也在每个 Python 进程中运行。
import os
import sys
import subprocess
# Simplified logic representation
def daemon():
# Locking mechanism and fork logic implied in original code structure
pass
- 对
/tmp/remote_code_loader.lock获取独占锁,确保每台主机只运行一个守护进程。 - 调用
fork()和setsid(),将标准输入输出重定向到/dev/null,并更改工作目录到/,从而脱离父进程。 - 每小时执行以下操作:
- 从攻击者的 GitHub 仓库读取
domain.txt,提取第一行看似域名的内容。 - 运行其内置的窃取器副本(该副本没有“每主机一次”的标记限制)。
- 下载
http://<domain>/codepy并使用exec()执行。
- 从攻击者的 GitHub 仓库读取
攻击者可以通过编辑 domain.txt 将守护进程指向新的服务器。每台受感染的主机都会运行该服务器返回的 codepy 内容。
在 UTC 13:40,GitHub 账户 letters9813 及其仓库返回 404 错误,此时距离版本发布仅过去约两小时。后门因此无法找到服务器地址而失效。但在删除之前运行过守护进程的主机,可能已经接收并执行了第二阶段代码。SafeDep 未能恢复具体的服务器地址。
事件时间线
所有时间均为 2026-10-21 UTC。
| 时间 | 事件 |
|---|---|
| 06:59 | 创建标签 v1.107.0 提交 66c8f5b |
| 08:50 | 在 main 分支上产生提交 a8113b0,作为发布源的基线 |
| 10:44 | 合法的 1.106.2 版本上传至 PyPI |
| 11:20 - 11:37 | 构建恶意的 1.107.0 Wheel 包 |
| 11:39 - 11:40 | 1.107.0 上传至 PyPI(共 8 个文件) |
| 11:50 | litellm-dispatcher[bot] 发布 GitHub 版本 v1.107.0 |
| 13:40 | 观察结果:1.107.0 已从 PyPI 移除,letters9813 账户已被删除 |
妥协指标 (IoC)
网络指标
| 指标 | 用途 |
|---|---|
www.litellm[.]dev, hxxp://www.litellm[.]dev/healthcheck | 接收窃取的数据 |
HTTP Header X-Token: XWiA8gZNa7NcX2XRoAEb | 随每次上传发送 |
raw.githubusercontent[.]com/letters9813/pythonclient-test/main/domain.txt | 后门在此读取服务器地址(账户已删除) |
hxxp://<domain>/codepy | 第二阶段代码下载 |
来自 Python 进程的 169.254.169.254 请求 | 窃取 AWS 实例凭证 |
主机指标
| 指标 | 含义 |
|---|---|
site-packages 中的 cook.pth, cook.py, sitecustomize.py | 植入物已安装 |
/tmp/.pc_once_72129118c59f5636c0bb | 窃取器已上传数据 |
/tmp/remote_code_loader.lock | 后门守护进程已运行 |
环境变量 PRIVCOLLECT_ACTIVE=1 | 存在于受感染的进程中 |
分离的 Python 守护进程(父 PID 为 1,独立会话,工作目录为 /) | 后门循环 |
文件哈希 (SHA-256)
| 文件 | SHA-256 |
|---|





