返回博客列表

LiteLLM 1.107.0 被投毒:一行 TOML,装包即中招

2026-10-24T12:50:00+08:00
LiteLLMPyPI供应链安全Python安全事件

LiteLLM 1.107.0 被投毒:一行 TOML,装包即中招

你 import 的每一行,都经过了一条你看不见的发布链。

安全公司 SafeDep 在 10 月 21 日披露:PyPI 上的 litellm 1.107.0 被植入了凭据窃取器和一个可远程下载执行的常驻后门。全部 7 个平台的 wheel(Linux、macOS、Windows)都带着恶意代码,安装后三个文件落进 site-packages,此后这台机器上任何一个 Python 进程启动时它们都会运行——哪怕这个进程从不 import litellm。PyPI 在当天 13:23 UTC 下架了该版本,LiteLLM 官方确认了事件并发布处置报告。

手法值得所有 Python 开发者细看:上游 GitHub 仓库里干干净净,一行加在构建配置里的 TOML 完成了投毒。这篇文章把事件完整拆开——植入原理、两个 payload、攻击者进来的路径,最后给一份排查与轮换清单。

本文提纲

  1. 事发:四个小时的窗口
  2. 植入原理:一行 TOML 与 site-packages 里的三个文件
  3. Payload 1:cook.py 凭据窃取器
  4. Payload 2:sitecustomize.py 常驻后门
  5. 攻击者怎么进去的:三个凭据与一个仿冒仓库
  6. 排查与轮换清单:装过就当全被偷了
  7. 给 Python 开发者的四条教训

事发:四个小时的窗口

LiteLLM 是调用各家 LLM API 最常用的统一网关库之一,B 站和各大公司都有生产部署。这次事件从攻击到止血,全程四个小时出头,时间线按 UTC(10 月 21 日)排:

时间 事件
10:08 攻击者覆盖分支保护,修改发布 workflow
10:46 预演构建,从仿冒仓库拉代码
11:39-11:40 七个恶意 wheel 上传 PyPI
11:46 恶意 Docker 镜像发布
13:23 PyPI 下架 1.107.0
13:40 攻击者控制的 GitHub 账号连同仓库消失
14:20 Docker Hub 相关标签移除或重指到干净镜像

窗口虽短,后果不小:Docker Hub 和 GHCR 上的 v1.107.0、latest、main-latest、main-stable 四个标签在 11:46 到 14:20 之间都是恶意的。CI 里写 pip install litellm 不锁版本的机器,这四个小时里拉到的就是带毒版本。

干净的版本:1.106.2、1.107.0rc1、1.107.0rc2;一起钉住的伴随包 litellm-proxy-extras==0.4.110 和 litellm-enterprise==0.1.78 也确认干净。

植入原理:一行 TOML 与 site-packages 里的三个文件

这次投毒最值得研究的不是恶意代码本身,是它进入发行包的方式。

恶意代码不在任何 git commit 里。SafeDep 对比后发现,上游 BerriAI/litellm 仓库没有任何公开 commit 包含这段代码或对应目录——sdist(源码分发包)与 tag 指向的 commit 对不上号。真正的注入点在 sdist 的构建配置里,pyproject.toml 的 maturin 段多了一行:

[tool.maturin]
# ...
python-source = "."
data = "cook"          # <-- 注入的一行
python-packages = ["litellm_cli"]  # 上游合法配置

maturin 的 data = "cook" 会把 cook/ 目录打进 wheel 的 purelib 区,pip 安装时整个目录解到 site-packages。sdist 里刻意不放 cook/ 目录本身,所以 diff 出来唯一的可见变化就是这一行 TOML。发布流水线拿到这份被改过的 sdist 一编译,签名、发布,流程全是官方的。

最终 wheel 的 RECORD 里多出三个文件:

文件 大小 作用
cook.pth 12 字节 内容只有 import cook,Python 启动时自动执行
cook.py 22,252 字节 凭据窃取器,一层 exec(zlib+base64) 混淆
sitecustomize.py 25,509 字节 常驻后门,Python 启动自动导入

这里的关键机制是 .pth 文件:Python 启动处理 site-packages 里的 .pth 时,凡以 import 开头的行都会被当代码执行。也就是说恶意代码的触发不依赖你写不写 import litellm——同一台机器上跑的 pip、IDE 语言服务器、CI 任务、任何 Python 脚本,启动即中招。

graph TB
    A["Exposed internal LiteLLM logs"] --> B["Stolen gh credentials of 3 developers"]
    B --> C["Org owner token bypasses branch protection"]
    C --> D["Release workflow points to lookalike repo"]
    D --> E["Approval gate passed with a second credential"]
    E --> F["Official pipeline builds and signs"]
    F --> G["7 malicious wheels on PyPI"]
    F --> H["Malicious Docker images"]
    G --> I["pip install litellm==1.107.0"]
    H --> I
    I --> J["3 files land in site-packages"]
    style A fill:#FF6B6B,color:#000000
    style B fill:#4ECDC4,color:#000000
    style C fill:#45B7D1,color:#000000
    style D fill:#96CEB4,color:#000000
    style E fill:#FFEAA7,color:#000000
    style F fill:#DDA0DD,color:#000000
    style G fill:#98D8C8,color:#000000
    style H fill:#98D8C8,color:#000000
    style I fill:#FFEAA7,color:#000000
    style J fill:#FF6B6B,color:#000000
    linkStyle default stroke:#764ba2

Payload 1:cook.py 凭据窃取器

解码后约 61 KB 的 Python。它带一张 16 项的收集清单模块,按目标分几类看:

类别 收集目标
主机与网络 主机名、用户、系统、IP 与网络配置
身份凭据 ~/.ssh/*、GPG 密钥、TLS 私钥
云凭据 AWS、Azure、gcloud、kubeconfig、Docker、gh、rclone、.netrc
CI/CD 密钥 GitLab runner、Jenkins 初始密码、Vault token、Terraform 凭据、Packer
开发者机密 $HOME 下的 .pem/.key/.pfx/.pk8 等证书私钥、Composer auth.json、FileZilla
AI 工具 Claude Code、Claude Desktop、Cursor、Windsurf、Copilot、VS Code mcp.json、Gemini CLI、Codex、Continue、Aider、Goose、opencode、Qwen、Crush、Kiro、Zed、Hugging Face token
浏览器 Chromium 保存的密码(DPAPI 解密)、Firefox logins.json 与 key4.db、浏览历史
Shell 历史 bash、zsh、PowerShell PSReadLine、cmd
容器与云元数据 Kubernetes service account token、AWS IMDS(先 v2 后 v1)实例角色凭据
其他进程 通过 /proc//environ 读其他进程的环境变量
LiteLLM 专属 读 DATABASE_URL,直接导出 proxy 数据库里的 LiteLLM_CredentialsTable 和 LiteLLM_ProxyModelTable——也就是网关代理的所有模型 API key

AI 工具那一张清单值得盯着看:攻击者把市面上叫得出名字的 AI 编码工具的凭据位置全列了一遍。这台机器上你的 Claude Code 登录态、Cursor 配置、Codex auth.json、甚至 VS Code 的 mcp.json(里面有 MCP server 的密钥)都在收集范围内。对一个装了 LiteLLM 的开发者机器来说,这可能是最值钱的部分。

外传方式做了两层加密:JSON 结果先用随机 AES-256-CBC 加密,AES 密钥再用内嵌的 RSA-4096 公钥(RSA-OAEP)包一层,打包成 tar.gz(hdr.json、data.bin、key.bin)POST 到伪装域名 www.litellm[.]dev/healthcheck,请求头带 X-Token: XWiA8gZNa7NcX2XRoAEb。域名挑得很讲究——不是生僻的陌生域名,是长得像官网的 litellm.dev,而 LiteLLM 官网在 litellm.ai。出口流量在监控里看起来像一次健康检查。

每台主机只偷一次:上传成功后写标记文件 /tmp/.pc_once_72129118c59f5636c0bb,进程内再设 PRIVCOLLECT_ACTIVE=1 防重复。

Payload 2:sitecustomize.py 常驻后门

sitecustomize 是 Python 启动时自动导入的约定模块,这个文件借同样的机制常驻。它 fork 出独立会话的守护进程(父 PID 归 1、工作目录切到根、输出全进 /dev/null),用 /tmp/remote_code_loader.lock 文件锁保证每台机器只有一个实例,然后每小时循环做三件事:

  1. 从 raw.githubusercontent[.]com/letters9813/pythonclient-test/main/domain.txt 读服务器地址
  2. 重跑内嵌的窃取器副本——这个副本没有每主机一次的标记,会反复收集
  3. 下载 http://<domain>/codepy 并用 exec 执行

这意味着偷凭据只是第一步。攻击者改一行 domain.txt 就能把全部感染主机指向新服务器,让它们执行任意二阶段代码。PyPI 下架救不了已经中招的机器。

万幸的是 GitHub 账号 letters9813 在发布约两小时后(13:40 UTC)连同仓库一起消失,后门现在读不到地址,处于空转状态。二阶段代码的服务器地址没能恢复——但 SafeDep 明确提示:在下架前跑过这个守护进程的机器,可能已经下载执行过二阶段代码,处置要按「已被完全接管」对待。

攻击者怎么进去的:三个凭据与一个仿冒仓库

按 LiteLLM 官方事件报告,攻击路径是这样的:攻击者掌握了三名开发者的 GitHub CLI 凭据,来源据信是一个暴露在公网的内部 LiteLLM 部署的日志——日志里留着这些凭据。拿到组织所有者 token 后,攻击者绕过分支保护,把私有的发布 workflow 指向了一个仿冒仓库:正版是 BerriAI/litellm,仿冒的是 Berri-AI/litellm,中间多个连字符。审批关卡用第二位开发者的凭据通过。然后官方流水线编译、签名、发布,一切看起来都合规。

这次事件里两个「看起来安全」的机制形同虚设,值得各记一笔。分支保护被组织所有者 token 绕过——权限的最高层没有更强的验证兜底。签名也签了,但签名的是被投毒的构建——签名链证明的是「这个包来自官方流水线」,而官方流水线本身已经不可信。

真正出问题的是源头:一次内部部署的日志暴露。日志系统是最容易被忽视的秘密汇集地——报错堆栈、调试输出、环境变量打印,都往里进。这批凭据在日志里躺了多久、被谁访问过,报告没有说,但攻击的起点确实在这。

排查与轮换清单:装过就当全被偷了

只要在 10 月 21 日 11:39 到 13:23 UTC 之间装过或重建过依赖(包括 CI 缓存),按中招处理。第一步确认版本,第二步查机器,第三步轮换。

# 1. 确认版本是否中招
pip show litellm | head -2

# 2. 查三个植入文件
ls "$(python -c 'import site;print(site.getsitepackages()[0])')" | grep -E '^cook|sitecustomize'

# 3. 查守护进程与标记文件
ls -la /tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock 2>/dev/null
ps -eo pid,ppid,command | grep python | grep -v grep  # 找 ppid=1 的 python 守护进程

主机侧的中招痕迹:site-packages 里有 cook.pth、cook.py、异常的 sitecustomize.py;/tmp 下有上述标记或锁文件;进程环境变量里有 PRIVCOLLECT_ACTIVE=1;存在父进程为 1、独占会话的 Python 守护进程。Docker 用户比对镜像摘要 sha256:1bbcb1443bc9...26c339e(v1.107.0、latest、main-latest、main-stable 四个标签的恶意版本)。

网络侧查日志:对 www.litellm[.]dev 的 POST 请求、带 X-Token 头的出站流量、Python 进程对 169.254.169.254(云元数据服务)的访问。

轮换范围按窃取器清单来,宁多勿少:SSH 与 GPG 密钥、全部云平台凭据(含 k8s、Docker)、CI/CD 所有密钥(GitLab/Jenkins/Vault/Terraform)、浏览器保存的全部密码、shell 历史里出现过的任何 secret、所有 AI 工具的登录态与 token、LiteLLM proxy 数据库里的全部模型 key。已知标记文件存在或守护进程跑过的,还要假设二阶段代码执行过——整个环境按失陷重建。

给 Python 开发者的四条教训

第一,.pth 自动执行是打包生态的长期盲点。任何包都能往 site-packages 放 .pth,一行 import 就能劫持全机 Python 进程。缓解靠收敛信任面:依赖用锁文件钉死版本,能开 hash 校验就开(pip install --require-hashes 或 uv 的 lock),CI 里更要钉。

第二,别只 diff 源码,要 diff 构建产物。这次上游 git 里干干净净,毒在 sdist 的构建配置和流水线产物里。新版本升级时留意安装后 site-packages 多出来的文件;PyPI 的 provenance 证明(PEP 458 方向)值得接入验证流程。

第三,签名验证的是管道不是善意。官方流水线被劫持时,签名只会给恶意包盖章。发布流程里真正缺的是对「构建输入」的强校验——workflow 从哪个仓库拉代码、sdist 与 commit 是否一致,这些要在流水线里机器化验证,不能靠人眼。

第四,AI 工具 token 已经是标准猎物。这次攻击者的收集清单几乎就是一份 AI 开发工具目录。给 AI 工具用短时效凭证、独立账号和最小权限,别把公司云的主密钥直接喂给它们——同理也适用于我们之前拆过的 ai-job-search 那类 Agent 项目,威胁模型里早写明白了:有文件权限的 LLM 加上不可信输入,这个组合只能收窄,无法消除。

供应链攻击没有「小事件」。一次装包,四小时窗口,收走的是一整台开发者机器的全部身份。把锁版本和校验当成默认习惯,是这类事件能留给我们的最实际的东西。

参考文档与链接

你的 requirements 里还写着不带版本号的 litellm 吗?评论区说说你的锁版本策略。觉得有用点个赞,转给你管 CI 的同事。


作者: itech001
来源: 公众号:AI人工智能时代(the-ai-era)
网站: https://www.theaiera.top/ 关注每日最新AI新闻和技术博客,主页有更多的文章的AI 技术参考:https://www.theaiera.top

本文首发于 AI人工智能时代,转载请注明出处。

分享给朋友