PyPI 上的 LiteLLM 1.107.0 版本携带凭据窃取程序和远程代码执行后门,全部 7 个 Linux、macOS、Windows 轮子把二者作为三个文件装进 site-packages;主机上任何 Python 进程启动时都会运行它们,即便从不导入 litellm。上游 GitHub 源码不含这些文件,PyPI 已下架该版本。1.106.2、1.107.0rc1 和 1.107.0rc2 干净,被 1.107.0 锁定的 litellm-proxy-extras==0.4.110 与 litellm-enterprise==0.1.78 也未被污染。
正文
PyPI 上的 LiteLLM 1.107.0 版本携带一个凭证窃取器和一个远程代码执行后门。全部七个 wheel(Linux、macOS、Windows)都会把它们作为三个文件安装到 site-packages。两者都会在主机上任何 Python 进程启动时运行,即使该进程从未导入 litellm。上游 GitHub 源码中并不包含它们。PyPI 已随后下架该版本。
1.106.2、1.107.0rc1 和 1.107.0rc2 是干净的。1.107.0 所固定的配套包 litellm-proxy-extras==0.4.110 和 litellm-enterprise==0.1.78 也是干净的。
攻击做了什么
安装 litellm==1.107.0 会在 site-packages 放入三个文件。Python 每次启动时都会加载其中的两个。
cook.pth导入cook.py,这是一个凭证窃取器。它收集 SSH 密钥、云凭证、CI/CD 秘密、AI 工具令牌、浏览器密码,以及 LiteLLM proxy 数据库中的 LLM 服务商密钥。它加密数据并发送到www.litellm[.]dev。sitecustomize.py启动一个后台守护进程。该守护进程每小时从 GitHub 上的一个文件读取服务器地址,再次运行窃取器,并从该服务器下载并运行代码。
植入物如何进入该版本
1.107.0 的源码分发包(sdist)中的 pyproject.toml,在 maturin 构建部分多出一行。这项 data = "cook" 设置会把 cook/ 文件夹复制到 wheel 里的 litellm-1.107.0.data/purelib/;pip 会把该文件夹安装到 site-packages。相关 TOML 共 6 行,文件为 litellm-1.107.0.tar.gz 中的 pyproject.toml。
sdist 本身不包含 cook/ 文件夹,所以这行是唯一可见的改动。BerriAI/litellm 的公开提交中没有任何一个包含 data = "cook" 或 cook/ 文件夹。
wheel 的 RECORD 列出了新增的三个文件:litellm-1.107.0.dist-info/RECORD 中的这段文本共 4 行。
该版本与它的 tag 不匹配
tag v1.107.0 指向提交 66c8f5b(UTC 06:59)。它的 pyproject.toml 写着 version = "1.108.0"。SafeDep 按 blob 哈希把 sdist 的每个文件与 git 树逐一比对。该 sdist 匹配 main 上更晚的提交 a8113b0(UTC 08:50),外加五个被修改的文件和 cook/ 文件夹。
其中四个被修改文件是 Cargo.toml,它们都新增了一行 readme = "README.md"。合法的 1.106.2 和 rc2 版本也有同样改动。第五个是 pyproject.toml,下面把它与 a8113b0 对比,差异为 11 行。
该修改把版本号改回 1.107.0,并把配套包固定到 rc2 的值。因此,与 rc2 做依赖对比看不出异常。合法的 1.106.2 sdist 与其 tag 提交 49943cd 相匹配,只差那些 Cargo.toml 的 readme 行。
wheel 是如何构建的
恶意 wheel 与 BerriAI 正常发布流水线的输出一致:
WHEEL文件标明maturin1.9.4,与合法发布中的版本相同。植入文件拥有正确的RECORD条目,说明构建后没有被改动过。- zip 时间戳显示各平台分别进行了原生构建:macOS 在 UTC 11:20,Linux 在 UTC 11:21,Windows 在 UTC 11:37。
- 在 Windows wheel 中,全部 2,804 个
.py文件都使用 CRLF 换行符,包括那三个植入文件;Linux wheel 则完全没有。在 Windows 上用启用换行符转换的 git checkout 会产生这种模式。
没有公开的 GitHub Actions 运行构建或发布了这个版本。SafeDep 检查了 UTC 09:30 到 12:30 之间的全部 539 次运行。在 UTC 11:50,即 PyPI 上传约 10 分钟后,litellm-dispatcher[bot] 发布了 GitHub release v1.107.0。1.106.2 也出现了同样的延迟。PyPI 没有 1.107.0、1.106.2 或 rc2 的来源证明(provenance attestations)。
SafeDep 以中到高的置信度评估:这些 wheel 是 BerriAI 的私有发布流水线基于被攻击者修改过的 git 源码构建的。攻击者可能利用了失陷的维护者或 bot 凭据、私有分支或镜像,或者流水线所构建的引用。从攻击者自己的机器上使用窃取的 PyPI token 与证据吻合度较低。只有 BerriAI 的流水线日志能确认原因。
载荷 1:cook.py 中的凭证窃取器
启动时,Python 会执行 site-packages 中 .pth 文件里每一行以 import 开头的内容。cook.pth 包含 import cook,因此窃取器会在主机上的每个 Python 进程中运行:包括应用程序、pip、IDE 语言服务器和 CI 任务。
cook.py 把约 61 KB 的 Python 代码包在一层 exec(zlib.decompress(base64.b64decode('...'))) 中。解码后的代码以一段配置开头(cook.py 解码后,10 行 Python)。
它收集什么
| 模块 | 目标 |
| --- | --- |
| sysinfo、netinfo | 主机名、用户、操作系统、IP 地址、网络设置 |
| browser_pw | Chromium 保存的密码,用 profile 主密钥解密(Windows 上为 DPAPI);Firefox 的 logins.json 和 key4.db |
| browser_hist | 浏览器历史记录 |
| ssh_keys | ~/.ssh/* |
| shell_hist | bash 和 zsh 历史、PowerShell PSReadLine、cmd 历史 |
| cloud_creds | ~/.aws/credentials 和 config、Azure profile 与令牌、gcloud 凭证、~/.kube/config、~/.docker/config.json、gh 的 hosts.yml、rclone.conf、~/.netrc |
| dev_secrets | $HOME 下的 .pem、.key、.pfx、.p12、.pk8、.ppk 文件,Composer auth.json,FileZilla 保存的服务器 |
| container_creds | Kubernetes service account token |
| cicd_secrets | GitLab runner config.toml、Jenkins initialAdminPassword 和 master.key、.vault-token、Terraform credentials.tfrc.json、Packer 设置 |
| ai_devtools | Claude 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_vars | 它自己的环境,以及其他进程通过 /proc/<pid>/environ 暴露的环境 |
| db_litellm | 读取 DATABASE_URL,并转储 LiteLLM_CredentialsTable 和 LiteLLM_ProxyModelTable 表;这些表保存 LiteLLM proxy 的 LLM 服务商 API 密钥 |
| cloud_metadata | AWS 实例元数据服务(先 IMDSv2,再 v1):实例身份和角色凭证 |
| gpg_keys、tls_privkey | GPG 密钥、TLS 私钥 |
它如何发送数据
- 它用随机 AES-256-CBC 密钥加密 JSON 结果,并用 RSA-OAEP 和内置公钥加密该密钥。
- 它把结果打包成 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 进程中运行。(sitecustomize.py 为 5 行 Python。)
- 它对
/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 未能恢复该服务器地址。
时间线
所有时间均为 UTC,日期为 2026-10-21。
| 时间 | 事件 |
| --- | --- |
| 06:59 | 创建 tag 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 release v1.107.0 |
| 13:40 | 观察到 1.107.0 从 PyPI 消失,letters9813 账号被删除 |
失陷指标(IoC)
网络
| 指标 | 用途 |
| --- | --- |
| www.litellm[.]dev、hxxp://www.litellm[.]dev/healthcheck | 接收窃取数据 |
| HTTP 头 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 |
| --- | --- |
| litellm-1.107.0.tar.gz | 2befe2a79fb4fbbd9a543b5b40b530c8cffa8771bab22b8fb22b2e4ae3b1f819 |
| litellm-1.107.0-cp310-abi3-manylinux_2_28_x86_64.whl | 462d0c9f67ca522d9e0e4acd245e90b36ce0d5353604a66bb7c43a1ec4a6fcf7 |
| litellm-1.107.0-cp310-abi3-manylinux_2_28_aarch64.whl | 668169d35b87bf51324080390b1b1c5ab12bd335cfe5f1a99e3689abd5e02ae6 |
| litellm-1.107.0-cp310-abi3-musllinux_1_2_x86_64.whl | 1bbce8caa94f01c0aaa0b7ddae51553a8a32e1ce53d96d44fe583272aff7ad00 |
| litellm-1.107.0-cp310-abi3-musllinux_1_2_aarch64.whl | c2e32d0ce2548aa662c18bf5001911a79fe5b30cf8357132275add02fdcde054 |
| litellm-1.107.0-cp310-abi3-macosx_10_12_x86_64.whl | 7598727f209d717041db37f7dca271d3a7d328a279890e0e1d55375729aeb86d |
| litellm-1.107.0-cp310-abi3-macosx_11_0_arm64.whl | 0aaf7ba346d41ef2f40390436738b001180d5a1c03aa093f121078a9969d9a1a |
| litellm-1.107.0-cp310-abi3-win_amd64.whl | 12d4ccc5c6f8d35cf4f2cf233b7f3e418e863e657d81ff5f5fc9e210005b9484 |
由于 CRLF 换行符,植入文件在 Windows 上的哈希不同:

| 文件 | Linux 和 macOS | Windows |
| --- | --- | --- |
| cook.pth | 7f191632e7320705d154b85204f1ae546b543acbd7e8861229b76f5042b92bdb | 84dc8c82c0a4a4a62552ef826b49fcc86d1a5c4a5d0f2c06665723f93d129d7d |
| cook.py | 9dd0a3926f96a37eb86cf1ee2310d9be93139cc4ebef3ef9e2efe9531390be25 | 03ac2a8b752605d1d7710979c17ff18b03dd978203ba23ba97fd8ce3d6f897ea |
| sitecustomize.py | 34bef04fe88ac9d0aae0475630b1738681d15b94f233ac8aa46eacf5968305fa | b6122440d0d9cb1eb1ade9933f49214da8c31288de7710c076c2680dfa528465 |

评论
社区情绪疲惫而愤怒:LiteLLM 短期内第二次发布窃取凭据的恶意 wheel 包,多数人认为这已构成失职并质疑维护者可信度,分歧在于根子应归咎于单个项目,还是整体供应链与系统隔离的缺失。少数人仍同情维护团队并借机推荐替代方案,或认为开源会随补丁变强;而把受限披露的修复优先放进付费企业私有仓库,也成为被集中批评的观感问题。
认为二次投毒属于严重失职,维护者可信度已受损
- 很多人指出,第一次被投毒尚可视为意外,第二次再发生就越过了可原谅的边界;有人因此庆幸已经迁移离开,有人直言项目在自毁声誉,还有人疑惑为何三年提交五万五千次的高活跃项目仍无法阻止构建被污染(较多人)
- > 这不可接受:一次可以原谅,两次就是失职(@stbenjam)
批评把修复优先放进付费企业私有仓库,观感和公平性都很糟
- 有人根据官方问题单指出,企业客户能从私有仓库获得受限披露的修复版,而开源用户被建议先固定到旧版本;这意味着不想被凭据窃取器影响就要付费,且还有第二个未公开漏洞的修复只分发给企业用户,被批评为“只是没给免费用户”(少数人)
- > 宣称有个只有企业客户付费才能用的私有仓库,不想在构建里被塞进凭据窃取器就得掏钱,这观感太糟(@petesergeant)
主张用默认冷却期、扫描和更强隔离来防供应链攻击
- 多名用户认为不能只靠 LiteLLM 自己:包管理器应默认对新版本设 24 到 48 小时冷却期,让人必须刻意绕开才能装更新;也有人希望有按用量付费、内置扫描与冷却的仓库托管,但现有方案往往很贵。系统层面同样被认为需要更容易的隔离环境,因为安装依赖时的权限弹窗疲劳和大量“顺手同意”会让沙箱规则失效(较多人)
- > uv、npm 等包管理器的默认配置应设 24 到 48 小时冷却期,想装更新版必须刻意绕开(@stbenjam)
少数人仍同情维护者,并借机推广替代方案
- 也有人祝愿 LiteLLM 团队再次处理好事故,并顺势推荐自己用 Go 写的、号称更快且更不容易被针对的替代项目(少数人)
- > 祝 LiteLLM 团队再次处理好此事,同时推荐一个更快、更不容易被盯上的 Go 语言替代方案(@santiago-pl)
少数人认为这只是开源生态的阵痛,会随补丁逐步变强
- 面对连环事故,仍有人保持乐观,认为今天虽然对开源是糟心的一天,但生态正是一个个补丁打强的(少数人)
- > 今天是开源槽糕的一天,但我猜我们就是这样通过一个个补丁变强的(@merelydev)
有价值的补充
- 攻击路径并非公开 PyPI 依赖劫持:分析认为 BerriAI 的私有发布流水线用被攻击者改过的 git 源码构建了 wheel,可能失陷的是维护者或机器人凭据、私有分支/镜像,或流水线引用的来源;被盗 PyPI token 由攻击者本机使用的解释力较弱。也就是说,恶意文件(讨论中提及 cook.pth)更可能在维护者本地副本或私有构建环节被“蹲点”注入,而不是所有人的依赖都被劫持。此判断转述自 @madamelic 引用的 JFrog 分析。
- 受影响面比直接使用者更广:即便没有直接使用 LiteLLM,Google ADK 等上游依赖也可能把它带入项目,增加传递依赖风险。(@rcleveng)
- 临时处置建议:官方事故说明中,开源用户被建议先把版本固定到 1.106.2 或此前的明确版本;企业客户若从私有仓库拉取则宣称不受影响。也有人提醒可用 canarytokens.org 部署蜜标,在凭据或文件被异常访问时收到告警。(@hmokiguess、@linsomniac、@yujonglee)
- 有用户用安装时“顺手同意几百个工具/升级”的麻木感作比:当包管理器报出大量待安装或待升级项时,用户会习惯性点“是”,说明权限弹窗和沙箱规则在自动化安装与智能体普及后会疲劳失效,因此操作系统需要更容易使用的独立“程序空间”或轻量 VM。(@crossroadsguy)