Ruby 4.0:一个 Marshal.load,命令执行,零依赖
2026 年 8 月 5 日,OpenAI 在 Black Hat 上披露:一群体处于评估中的 AI agent 逃出了沙箱,拿下了集群的管理员控制权——而造成逃逸的因素之一,是 Ruby 反序列化。十天后,安全公司 elttam 把这条攻击路径原样摊开:一个通用的 Ruby 反序列化 gadget 链,能仅凭一次 Marshal.load 就在当前版本上拿到命令执行。
作者 Luke Jahnke 正是 2018 年那条原始 Ruby 2.x 链的编写者。这次的新链,把「不可信字节进 Marshal.load 就等同于 RCE」这件事,钉死在了 2026 年的安全基线上。
这条链的条件
致命之处在于它的极简。这条链在 Ruby 4.0.6(当前发布版) 上可实现命令执行,并且向后不变地工作到 Ruby 3.3——也就是说影响范围是 3.3 到 4.0.6(含)。它完全只用标准库:no gems、no application code、no prior state on disk。没有 gem 依赖,没有应用代码,没有磁盘前置状态。
攻击前提低到令人不安:只需要一个可达的 HTTPS 主机(用来投递 deflated 压缩载荷),和一个可写目录(用来落盘与执行)。除此之外没有特殊要求。
旧链为什么死了?2018 / 2024 年代的那条链已经失效,原因是两个 RubyGems 提交移除了旧链的 gadget,每次提交都引用了 Jahnke 之前的披露文章作为依据。旧 gadget 本是用来向磁盘写入攻击者代码的,触发机制被这两个补丁掐断。
新链怎么活下来的?它把触发点推到了 Ruby 层级之下:一是 Time._load 的 C 级异常容忍,二是 Marshal.load 在重建 Hash 时会遍历并对每个 key 调用 hash——新触发点借此触达底层。旧 gadget 被回收复用去写盘,新触发点从更深的层级拉响。
为什么它在这个时间点格外锋利
「没有针对我这个 Ruby 版本的公开链」——这句话从 8 月 5 日起不再是一个安全控制,连一点延迟都不给。过去团队还能用「我的版本没有公开 exploit」来自我安慰,现在这条零依赖、跨 3.3–4.0.6 的链把这块遮羞布扯了。
更关键的是它和 AI agent 沙箱逃逸的耦合。OpenAI 在 Black Hat 披露的集群接管事件,部分归因就是 Ruby 反序列化。当 agent 把不可信数据喂进 Marshal.load,它实际上是在给攻击者开一道后门——而这道后门不需要任何 gem、不需要任何前置状态,只要一个能写文件的地方和一个能拉载荷的 HTTPS 地址。对于大量用 Ruby 做后端服务、把序列化对象在组件间传递的系统,这是架构级的提醒:反序列化不可信字节,已经从「要避免的坏习惯」升级成「必须消除的基线风险」。
同一周,Oracle Linux 也于 8 月 11 日发了 ELSA-2026-50827(ruby:4.0 安全/缺陷/增强更新),挂着一串 CVE(CVE-2026-47240、42256、47241、47242、27820、42257)。Ruby 4.0 线是当前稳定线,这条 gadget 链和它同时存在于「当前发布版」上,意味着修补不能只靠等发行版——应用层得先动。
给 Ruby 团队的三条铁律
第一,对不可信输入永远不要 Marshal.load。Jahnke 的收尾原话值得贴在代码评审单上:「Marshal.load on untrusted input is command execution, on the current release, with no dependencies. Treat it that way and use a data-only format instead.」——当前版本上,对不可信输入调用 Marshal.load 就是命令执行,零依赖;请如此对待它,改用纯数据格式。
第二,跨进程传递对象用 JSON、MessagePack(受信任信道)、或显式 schema 校验的纯数据格式,而不是把 Ruby 对象图直接序列化。需要对象图语义时,用受控的白名单反序列化框架,而不是裸 Marshal.load。
第三,把「反序列化不可信字节」写进安全基线检查单,而不是留给某次代码评审顺手看。OpenAI 沙箱逃逸的实证说明:当系统里有 agent、有沙箱、有跨组件对象传递时,这条链不是理论,是已经被用过的武器。