05 - 供应链复盘:安全扫描器成了攻击入口
前置:03 - 工具投毒与 Rug Pull 里"不固定版本"的风险。
本篇回答:一次完整的、真实的 Agent 基础设施攻击,从头到尾是怎么发生的?
会用到的词:
- 供应链攻击:不打你,打你依赖的东西
.pth文件:Python 的一个特性 —— 放在site-packages/下、以import开头的.pth文件,会在每次 Python 解释器启动时被自动执行- IOC(Indicator of Compromise):入侵指标,比如恶意域名、文件哈希
一、这次事件的特殊之处
04 篇的结论是"网关是最好的防线位置"。这一篇讲的是那道防线自己被打穿的那一天。
被打的是 BerriAI/litellm —— 本站 Agent 网关专题拆了整整十篇的那个项目,★56,709,月下载量约 9,500 万。
它是很多公司唯一持有全部模型 API 凭证的组件。
二、时间线
从投毒到发布干净版本,六天。 这个响应速度在开源项目里已经算快的了 —— 但对于一个凭证窃取型攻击来说,前 40 分钟就已经决定了一切。
三、攻击链
这是整件事最值得反复讲的部分。
官方 issue 里 LiteLLM 团队的原话:
Compromise came from trivvy security scan dependency
攻击者没有打 LiteLLM,他们打的是 LiteLLM 用来做安全检查的那个工具。
这件事的讽刺程度值得停下来想一想:你在 CI 里加一个安全扫描器,是为了提高安全性。而这个扫描器本身成了攻击入口 —— 它天然拥有读取仓库、访问 CI 密钥的权限。
💡 可以直接拿走的教训:CI 里的每一个工具都是你的攻击面,尤其是那些需要高权限的安全工具。它们通常被无条件信任,因为"它是来保护我们的"。
3.1 一个关键的辨识信号
issue 里写得很清楚:
The attacker published malicious versions to PyPI that were never released through the official GitHub CI/CD. GitHub releases only go up to
v1.82.6.dev1— versions 1.82.7 and 1.82.8 on PyPI were uploaded directly by the attacker.
PyPI 上的版本号,比 GitHub Release 上的版本号更新。 这是一个任何人都能发现的异常信号。
能拿走的自查方法:定期比对关键依赖在包仓库和源码仓库上的版本一致性。这是可以自动化的,而且几乎零成本。
四、Payload 的两个版本
| 版本 | 投毒方式 | 触发条件 |
|---|---|---|
| 1.82.7 | payload 藏在 litellm/proxy/proxy_server.py | import litellm.proxy 时 |
| 1.82.8 | 新增 litellm_init.pth(34,628 字节)+ proxy_server.py 里的 payload | 任意 Python 进程启动 —— 完全不需要 import |
4.1 .pth 文件为什么致命
Python 有一个鲜为人知的特性:site-packages/ 目录下的 .pth 文件,如果某一行以 import 开头,这一行会在解释器启动时被自动执行。
它的本意是让包能在启动时做一些路径注册,但后果是:
- 你不需要
import litellm - 你甚至不需要用到 LiteLLM
- 只要这个包装在你的环境里,你在这个环境里跑任何一行 Python,payload 就执行了
# 这些全都会 触发
python -c "print(1)"
pytest
pip list
你项目里任何一个和 litellm 毫无关系的脚本
从 1.82.7 到 1.82.8,攻击者做的这一步升级,把感染面从"用了这个库的进程"扩大到了"装了这个库的整个环境"。
4.2 窃取的内容
官方 issue 的原文清单:
- Collects: SSH keys, environment variables (API keys, secrets), AWS/GCP/Azure/K8s credentials, crypto wallets, database passwords, SSL private keys, shell history, CI/CD configs
- Encrypts: AES-256-CBC + RSA-4096 (hardcoded public key)
- Exfiltrates:
curl POSTtohttps://models.litellm.cloud/
三点观察:
1. 目标不是 LLM 相关的东西,是整台机器上的一切凭证。 SSH 私钥、云凭证、K8s secret、数据库密码、SSL 私钥、shell history —— 一次得手,你的整个基础设施都暴露了。
2. 加密外传(AES-256-CBC + 硬编码 RSA-4096 公钥)。 这不是为了保护你的数据,是为了让流量审计看不出传的是什么,同时保证只有攻击者能解密。这是成熟的攻击工具,不是脚本小子。
3. 外传域名 models.litellm.cloud。 注意 —— 官方域名是 litellm.ai,攻击者用的是 litellm.cloud。
这个域名注册于 2026-03-23,比恶意包上架早了几个小时。
一个只看域名字符串"眼熟不眼熟"的人,很难在告警里把它挑出来。出站域名白名单必须是白名单(只放行已知的),不能是黑名单或者"看着像就放行"。 这正好呼应 04 篇里那条"出站域名白名单"的建议。
五、未受影响的部署形态
官方的这条说明是整个事件里最有价值的一句:
Proxy Docker image users were not impacted, all dependencies are pinned on requirements.txt
用官方 Docker 镜像的用户毫发无损,因为镜像里的依赖是钉死版 本的。
把这条和 Agent 网关 · 02 - 四种形态对照着看:
| 部署形态 | 这次的结局 |
|---|---|
pip install litellm 当库用 | ⚠️ 中招,且 .pth 让整个 Python 环境暴露 |
| 官方 Docker 镜像跑 Proxy | ✅ 未受影响 |
同一个软件、同一个漏洞,两种形态的后果完全不同。
而且注意方向:作为库引入是最脆弱的 —— payload 直接落进了你的业务进程所在的环境,能拿到那台机器上的一切。作为独立进程(容器)跑,爆炸半径被限制在容器内,而且钉死的版本从一开始就挡住了恶意版本。
这也验证了 03 篇里 SkillSpector Rug Pull 检测器那几条规则的价值 —— 它们检查的就是"有没有钉版本":
remediation="Pin the version: pip install package==1.2.3"
一条几十年历史的、无聊的、跟 AI 毫无关系的工程实践,在这次事件里是唯一有效的防线。
六、攻击者对社区讨论的干扰
issue 里有这么一段:
Attacker behavior: The attacker appears to be publishing hundreds of spam comments to suppress discussion.
原始的技术分析 issue #24512 被攻击者用垃圾评论刷到关闭。
攻击的一部分是让受害者更晚发现。 40 分钟的窗口期里,能拖延一分钟就多一分钟的收获。
这对应急响应的启示是:事故沟通渠道本身要有备份。 LiteLLM 团队当时建议大家转到 Hacker News 的讨论串。如果你的唯一沟通渠道是攻击者也能写入的地方,那它在事故中不可靠。
七、官方做了什么
从 issue 里摘出的处置动作:
| 动作 | 说明 |
|---|---|
| 删除恶意包 | v1.82.7、v1.82.8 |
| 轮换全部 maintainer 账号 | 新账号 @krrish-berri-2、@ishaan-berri |
| 暂停发版 | 直到确认供应链干净 |
| 引入外部力量 | 官方称已联系 Google Mandiant 团队 |
| 排查影响面 | 审查所有 berriai 仓库、扫描 CircleCI 构建 |
| 重建 CI/CD | 03-30 发布 1.83.0,走全新的 CI/CD v2 流水线 |
| 公开说明会 | 03-27 举行 Security Townhall |
PyPI 侧的反应更激烈:整个 litellm 包一度被下架,所有版本都返回 "No matching distribution found"。
💡 注意这条对你的影响:包被下架的那段时间里,所有钉了旧版本、且没有本地缓存的构建全部失败。也就是说,即使你没中招,你的 CI 也会挂。
事故响应计划里要包含这一条:上游包被下架时,你还能不能构建? 私有镜像源在这时候是唯一的答案。
八、这次事件对应 OWASP 的哪一条
01 篇的 ASI 清单里,这是 ASI04 — Agentic Supply Chain Compromise 的教科书案例。
但它同时命中了另外两条:
- ASI03(身份与权限滥用):拿到的是 maintainer 的发布权限
- ASI05(意外的代码执行):
.pth让代码在完全没预期的时机执行
真实事故从来不只对应一个条目。 威胁清单的用途是排查,不是分类。
九、七项应对措施
| # | 动作 | 成本 |
|---|---|---|
| 1 | 所有依赖钉死版本,用 lock 文件 | 低 |
| 2 | AI 网关用容器跑,别当库 import | 低 |
| 3 | 自动比对关键依赖的 PyPI / npm 版本与 GitHub Release 版本 | 低 |
| 4 | 出站域名白名单 —— 只放行已知域名 | 中 |
| 5 | 审计 CI 里的每一个工具,尤其是安全工具 | 中 |
| 6 | 搭私有镜像源,保证上游下架时还能构建 | 中 |
| 7 | 演练一次"全部凭证轮换",测出实际需要多久 | 高 |
第 7 条最容易被跳过,也最重要。这次事故的官方建议是 rotate ALL credentials —— 如果你从来没演练过,真出事那天你会发现有些凭证根本不知道存在哪、谁在用、改了会挂掉什么。
十、全专题结论
| 问题 | 答案 |
|---|---|
| Agent 安全的根本难点是什么? | 指令和数据在同一个上下文里,提示注入挡不住(01) |
| 那怎么办? | 不追求"不被注入",追求"被注入了也干不了大事" |
| 协议层在做什么? | 把信任边界画得更细:角色分离、令牌绑定、iss 校验(02) |
| 攻击具体长什么样? | 隐藏注释、零宽字符、同形异义字、未固定版本(03) |
| 防线放哪儿? | 网关是唯一同时看得见身份和意图的位置(04) |
| 网关自己被打穿呢? | 钉版本、用容器、白名单出站(本篇) |
一条贯穿全专题的主线:这个领域里真正有效的防御,几乎都不是 AI 技术,而是几十年的老工程实践 —— 最小权限、默认拒绝、钉版本、白名单、职责分离。
新的是攻击面,不是解法。
← 回到 专题索引 · Agent Infra 板块总览