Fork 炸弹救了我们
LiteLLM 1.82.8 中的恶意代码藏在一个 .pth 文件里,任何 Python 启动时它都会运行。它会收集 SSH 密钥、云凭据、加密货币钱包和 CI/CD 密钥,用 4096 位 RSA 密钥加密后,把打包好的数据外传到攻击者控制的域名。载荷做得很扎实,加密没有破绽,外传过程干净利落。1 这起事件属于我的代理安全系列——关注那些真实发生的失败,以及它们如何塑造我们对自动化系统的信任方式。5
问题出在,这个 .pth 文件会派生一个 Python 子进程去干活。而子进程启动时,又会触发 .pth 文件。于是再派生一个子进程,再触发一次。指数级的 fork 炸弹在几秒钟内吃满 100% CPU 和 5 GB 以上内存。2
fork 炸弹是个 bug。攻击者并不希望恶意代码暴露。如果实现正确,它本可以在每一台被感染的机器上、每一次 Python 调用时悄无声息地运行,可能持续数周。结果开发者发现自己的机器卡到几乎瘫痪,一查,揪出了这个凭据窃取程序。发布后 46 分钟,PyPI 隔离了这两个版本。1
四万六千次安装,四十六分钟。所谓的检测机制,其实是恶意代码里的一处实现错误。
当 AI 代理的某个依赖装进了含有 fork 炸弹的软件包时,指数级的进程派生会在几秒内占满 100% CPU 和数 GB 内存,机器随即无法使用。 在 LiteLLM 1.82.8 这起事件中,这个意外的 fork 炸弹是凭据窃取载荷被发现的唯一原因:开发者注意到机器卡死,才去追查。若没有这个 bug,恶意代码会悄悄外传 SSH 密钥、云凭据和加密货币钱包,数周之后才可能被发现。
摘要
- 那个 bug:LiteLLM 1.82.8 的凭据窃取程序带着一个 fork 炸弹 bug,把被感染的机器拖垮。没有这个 bug,它会安静地跑上好几周。
- 那个缺口:静态分析、行为监控、代码审查,全都没能拦下这次攻击。每一层检测都指望另一层会兜住,结果谁也没兜住。3
- 那条曲线:攻击者的水平会随着迭代提升。
.pth技术如今已被公开记录在案。下一个攻击者继承的是不带 bug 的版本。 - 不靠运气也管用的做法:出站流量的域名年龄检查、软件包安装的行为基线、文件系统诱饵文件(canary)、安装环境隔离。这几项都与载荷质量无关,照样有效。
- 不对称之处:环境由防守方来定。如果安装环境里根本没有凭据可偷,再完美的载荷也一无所获。
我们只是运气好
把 fork 炸弹从载荷里去掉,这次攻击就会在无声无息中得手。.pth 文件的执行早于任何 import,早于任何应用代码,也早于任何 Python 层面的沙箱。没有钩子挂载点,没有日志记录。凭据窃取程序跑完、加密、外传,Python 进程一切照旧。开发者什么都看不见。CI 流水线什么都看不见。安全扫描工具也什么都看不见——因为安全扫描工具本身就是这次的攻击入口。3
LiteLLM 1.82.8 的发现过程,不是“我们的监控抓到了它”,而是“攻击者写出了一个 bug”。
把供应链安全建立在这样的基础上,实在让人不安。正如我在你的代理沙箱只是个建议一文中所说,我们以为存在于可信代码与不可信代码之间的那道边界,远比大多数团队想象的要疏漏得多。
攻击者的质量曲线
软件质量随迭代而提升。这条规律对攻击者和防守方同样成立。TeamPCP 的行动一周之内打穿了五个生态:GitHub Actions、Docker Hub、npm、Open VSX 和 PyPI。4 每攻陷一个生态,用的都是从上一个生态里收割来的凭据。整场行动的操作水准相当老练:在投递载荷前 24 小时注册域名,对可变引用做标签劫持,还利用 Aqua Security 密钥轮换不彻底的漏洞绕开了凭据轮换。
在一场本来颇为老练的行动里,fork 炸弹是唯一的失手。下一场行动不会再犯这个错。.pth 文件技术如今已被公开记录,CrowdStrike、Microsoft、Wiz 和 Palo Alto 都做过分析。3 下一个攻击者继承的是这套技术,但不带那个 bug。
攻击能力和防御能力沿着同一条曲线成长。技术是公开的,分析也是公开的。下一个攻击者的起点,就是 TeamPCP 的终点。关于这条曲线对自主系统意味着什么,我在无人监督时真正会出问题的地方里展开谈过。
检测不能寄望于攻击者失手
当前的供应链检测模型分三层,面对 LiteLLM 这三层全军覆没:
静态分析没查出来。.pth 文件是 Python 的正规特性。载荷经过两层 base64 编码,运行时才解码。那些靠已知恶意特征匹配的静态扫描器一无所获,因为这个特征是新的。
行为监控没查出来。凭据窃取程序只发了一个出站 HTTPS POST 请求,目标域名看着像个正经服务(models.litellm.cloud)。检查目标域名的出站监控,得知道这个特定域名是 24 小时前才注册的才行。而大多数出站监控根本不查域名年龄。
代码审查没查出来。恶意版本是直接发布到 PyPI 的,整条 GitHub CI/CD 流水线被完全绕过。没有 pull request 可审,没有 diff 可看。攻击者用盗来的发布凭据上传了预先构建好的软件包。
每一层检测都以为攻击链的另一环会兜住问题。结果谁也没兜住。兜住问题的是那个 fork 炸弹。
什么才真能发现无声的恶意代码
既然不能指望攻击者失手,就需要那些不依赖实现质量的检测手段。
对出站请求做域名年龄检查。 外传域名是在攻击前 24 小时注册的。一条防火墙规则,把发往注册不足 7 天的域名的出站请求标记出来,本可以发现这次攻击。规则很简单,误报率也在可控范围内,而且它正好覆盖了最常见的外传模式。
为 Python 进程建立行为基线。 一次 pip install 突然向陌生域名发起 HTTPS POST 请求,这本身就反常。进程级的行为监控只要跟踪软件包安装期间的网络活动,就能把它标出来。
文件系统诱饵文件。 在一个诱饵路径下放一把假的 SSH 密钥,另一个诱饵路径下放一份假的 AWS 凭据,然后监控是否有进程读取这些文件。凭据窃取程序会把标准路径扫一遍,必然读到诱饵;正常进程则不会。诱饵会在外传完成之前触发告警。
安装环境隔离。 在一个拿不到真实凭据的环境里执行 pip install,装完之后再把软件包复制到生产环境。.pth 文件是在 pip 自己的 Python 进程中触发的,也就是说凭据窃取程序在安装阶段就已经跑了。只要安装环境里没有凭据可偷,这次攻击就什么也拿不走。
以上这些手段,没有一项需要攻击者失手,它们与载荷质量无关,照样奏效。这种架构思路——把环境设计成即便攻击完美无缺也一无所获——和部署与防御:代理信任悖论背后的原则是同一个。
不对称之处
防守方有一个结构性优势:环境由防守方来定。攻击者只能在软件包被装进去的那个环境里施展。如果那个环境没有凭据、没有网络访问权限,还布着文件系统诱饵文件,那么载荷在技术上成功了,在实际效果上却是彻底失败。
LiteLLM 这次攻击之所以奏效,是因为安装环境和存放发布凭据、SSH 密钥、云令牌的,是同一个环境。fork 炸弹对安全架构本身毫无影响,它影响的只是暴露的时间早晚。
下一次,fork 炸弹不会再出现。而凭据依然会和包管理器待在同一个环境里。问题在于:在下一个攻击者交付一份干净的载荷之前,你有没有把环境改掉。我在 Ralph 代理架构的分析里讲了如何组织代理系统,让被攻陷的组件无法越过自身的隔离边界扩大战果。
常见问题
攻击者为什么没测出这个 fork 炸弹?
让 .pth 文件派生一个子进程来跑载荷,本身是个合理的实现选择,可以避免阻塞父进程。递归触发源于 .pth 与 Python 的 site.py 初始化之间一处微妙的相互作用。这类 bug 通常在集成测试中才会暴露,单元测试里看不出来,而恶意代码作者很难在贴近真实的环境中做集成测试。
fork 炸弹会不会是故意的?
可能性很小。fork 炸弹让恶意代码立刻暴露,这与攻击者的目标背道而驰。一个安静运行数周的凭据窃取程序,收割到的凭据比一个 46 分钟内就被发现的要多出好几个数量级。
域名年龄检查在大规模场景下可行吗?
可行。域名年龄可以通过 WHOIS 或 DNS 注册日期 API 获取。这项检查给每个请求增加的延迟只有毫秒级。大多数组织都可以把已知的新域名加入白名单。
参考来源
-
FutureSearch (Daniel Hnyk), “LiteLLM Hack: Were You One of the 47,000?” March 2026. ↩↩
-
isfinne et al., “LiteLLM Supply Chain Attack,” GitHub Issue #24512, March 2026. ↩
-
Blake Crosley, “The Supply Chain Is the Attack Surface,” blakecrosley.com, March 2026. ↩↩↩
-
Kaspersky, “Trojanization of Trivy, Checkmarx, and LiteLLM Solutions,” March 2026. ↩
-
Blake Crosley, “When Your Agent Becomes the Researcher,” blakecrosley.com, March 2026. ↩