Ruby Marshal反序列化漏洞演进史:从简单漏洞到工业化利用

本文详细追溯了Ruby Marshal反序列化漏洞的完整演进历程,从2013年的首个漏洞报告到2024年的工业化利用工具。文章分析了漏洞利用技术的演变模式,包括基础漏洞利用、通用gadget链发现以及现代程序分析技术的应用,并提出了从根本上解决Marshal安全问题的建议。

Marshal疯狂:Ruby反序列化漏洞利用简史

理解Marshal反序列化漏洞

2024年12月12日,Ruby语言的CI服务器某处刚刚发布了3.4.0-rc1版本。Ruby开发者们并不知道,存在一个微妙的bug允许进行Marshal反序列化利用。这段代码已经16年未被修改,因此没有任何理由怀疑存在问题。然而,几周前Luke Jahnke发布了一种新的Marshal利用技术,但消息尚未传开。就在圣诞节前,补丁被合并,Ruby 3.4.0最终发布时已不包含易受攻击的代码。危机得以避免。

如果未打补丁的代码被发布,那么类似下面的Rails控制器将会存在漏洞:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class UserRestoreController < ApplicationController
  def show
    # 某处的用户数据恢复控制器...
    user_data = params[:data]
    if user_data.present?
      deserialized_user = Marshal.load(Base64.decode64(user_data))
      user_object = UserObject.new(data: deserialized_user)
      user_object.save!
      render plain: "User data saved successfully: #{deserialized_user.inspect}"
    else
      render plain: "No data provided", status: :bad_request
    end
  end
end

图1:执行Marshal反序列化的Rails控制器

通常你不会看到如上所示的人为代码片段,但Marshal格式经常出现在后端系统中,比如缓存层或在文件系统上存储Ruby对象时。简而言之,将不受信任的输入传递给Marshal.load应被视为任意代码执行漏洞。

构建此类漏洞的利用程序超出了本文的范围,已在下面链接的资源中详尽介绍(参见Jahnke 2024)。但通常需要向图1中的Rails控制器发送如下所示的Marshal格式字节序列:

1
"\x04\b[\ac\x15Gem::SpecFetcherU:\x11Gem::Version[\x06o:\x1EGem::RequestSet::Lockfile\n:\t@seto:\x14Gem::RequestSet\x06:\x15@sorted_requests[\ao:%Gem::Resolver::SpecSpecification\x06:\n@speco:$Gem::Resolver::GitSpecification\a:\f@sourceo:\x15Gem::Source::Git\n:\t@gitI\"\bzip\x06:\x06ET:\x0F@referenceI\"\x10/etc/passwd\x06;\x10T:\x0E@root_dirI\"\t/tmp\x06;\x10T:\x10@repositoryI\"\bany\x06;\x10T:\n@nameI\"\bany\x06;\x10T;\vo:!Gem::Resolver::Specification\a;\x14I\"\bany\x06;\x10T:\x12@dependencies[\x00o;\n\x06;\vo;\f\a;\ro;\x0E\n;\x0FI\"\bzip\x06;\x10T;\x11I\"*-TmTT=\"$(id>/tmp/marshal-poc)\"any.zip\x06;\x10T;\x12I\"\t/tmp\x06;\x10T;\x13I\"\bany\x06;\x10T;\x14I\"\bany\x06;\x10T;\vo;\x15\a;\x14I\"\bany\x06;\x10T;\x16[\x00;\x16[\x00:\x13@gem_deps_fileI\"\x11/private/tmp\x06;\x10T:\x12@gem_deps_dirI\"\r/private\x06;\x10T:\x0F@platforms[\x00"

图2:Marshal反序列化利用payload示例

幸运的是,前面提到的新利用向量在Ruby 3.4.0发布前就被修补了,但正如我们将看到的,修补最终会变成徒劳的练习。要理解我们如何走到这一步,首先必须了解我们来自哪里。

开端:一个不起眼的bug跟踪问题

最初,Charlie Somerville(现名Hailey)于2013年1月31日创建了一个Ruby bug跟踪问题。该问题讨论了Ruby 2.0.0版本中Marshal.load的危险性。在创建此问题之前,Hailey和Ruby团队之间可能通过安全邮件列表进行了一些私人通信,但我将此问题称为Marshal反序列化利用谱系的开始。

在急于寻找更早的例子之前,请考虑我在此要表达的观点:Hailey的问题与现代Ruby反序列化利用开发之间存在直接联系。本文不是要找到Marshal反序列化漏洞的最早引用,而是要追溯思想和利用开发的演变过程。

从这里开始,故事在2016年5月6日的Phrack #69中重新开始。啊,Phrack,我们的老朋友。幸好这篇Phile进入了#69期,因为#70期要五年多后才出版。Marshal反序列化利用的概念出现在Phrack期刊某期某篇Phile的某个小节中。它针对Ruby on Rails 3和4版本中的Marshal反序列化。该Phile的作者joernchen直接引用了Hailey Somerville,然后立即声明该技术在Rails 4.1中已被修补,除非你修改默认行为。一个利用程序的生与死。这标志着开端的结束。从此以后,Ruby反序列化利用将在流行度和创造性上爆炸性增长。

爆发:安全研究人员的即兴创作

2018年11月8日,Luke Jahnke发表了一篇题为"Ruby 2.x Universal RCE Deserialization Gadget Chain"的博客文章。这个gadget链针对Ruby 2.x中的Marshal反序列化。在这个故事中,我们首次看到了利用gadget的程序化搜索——即使用基本的程序分析来搜索Ruby标准库和其他常见库中的利用gadget。利用gadget是可以链接在一起创建payload的代码片段,使恶意操作如任意代码执行或任意文件下载成为可能。这些payload通常就是我们认为是"利用程序"的东西。因此,Luke发布了一种生成Marshal反序列化利用payload的技术。他还确保引用了Phrack文章和Hailey Somerville。

现在有趣的部分开始了。在相对快速的时间间隔内,发布了以下内容:

日期 内容 引用 补丁
2019年1月2日(2019年3月19日公开披露) ooooooo_q为Rails 5.2 Marshal反序列化bug打开HackerOne报告。此bug获得CVE-2019-5420。 Rails 5.2.2.1并向后移植到其他受支持版本
2019年3月2日 Etienne Stalmans发布"Universal RCE with Ruby YAML.load",针对Ruby 2.x中的YAML反序列化。 Luke 2018年的工作 Ruby 2.7.2和Rails 6.1
2019年6月20日 Zero Day Initiative发布博客文章。 ooooooo_q 2019年的工作
2021年1月7日 William Bowling发布"Universal Deserialisation Gadget for Ruby 2.x-3.x",针对Ruby 2.x和3.x。 Luke 2018年的工作 Ruby 3.1.0(根据2b17d2f)
2021年1月9日 Etienne Stalmans发布"Universal RCE with Ruby YAML.load (versions > 2.7)",包含利用该链的完整YAML payload。 Luke 2018年和William 2021年的工作
2022年4月4日 William Bowling发布"Round Two: An Updated Universal Deserialisation Gadget for Ruby 2.x-3.x",针对具有最新补丁的Ruby 2.x和3.x。 Luke 2018年和William 2021年的工作 Ruby 3.2.0

活动如此频繁,以至于ooooooo_q甚至写了一本名为《Deserialization on Rails》的书。然而,从那里开始,事情安静了一段时间。发生了两件事:Ruby 3.1.0和Psych(YAML)4.0.0使安全YAML加载成为默认设置,Ruby 3.2.0修补了Marshal反序列化gadget。反序列化利用似乎正在减少,但黑客总会找到方法。我认为下一个时代是"现代时代"。它继续将bug搜索和程序分析相结合,以推动技术发展。

现代时代:强大的gadget发现

我认为Ruby反序列化利用现代时代的一个决定性特征是工业化、临床化的方法。不再是个人在业余时间进行黑客攻击。现代时代见证了专业人员使用"工业级"工具来压倒防御者。这在许多方面反映了整个安全行业。漏洞研究和利用开发已不是10到15年前的样子。猫鼠游戏保持不变,但工具和技术显著更先进。而且有更多的组织愿意为此付费。

现代时代以Include Security的Alex Leahu于2024年3月13日发布的博客文章"Discovering Deserialization Gadget Chains in Rubyland"拉开序幕。他引用了Luke和William的博客文章,并使用grep搜索利用gadget。他最终使用Rails库创建了一个利用链。缺点是此链在Rails环境之外无法工作。然而,在此文章发布后不久,另一篇文章将一切提升到了新的水平。

2024年6月20日,Peter Stöckli和GitHub Security Lab发布了一篇博客文章"Execute commands by sending JSON? Learn how unsafe deserialization vulnerabilities work in Ruby projects"。该文章描述了他们对Ruby反序列化利用的研究,涉及JSON、XML、YAML,当然还有Marshal数据格式。它提供了执行过程间分析的CodeQL查询,以确定你的序列化使用是否容易受到利用。它为利用前面提到的所有数据格式中的反序列化bug提供了概念验证payload。简而言之,这与Hailey Somerville 2013年的Ruby bug跟踪问题有很大不同。当然,它引用了William的通用反序列化gadget文章。

这就是我对现代时代的调查本应结束的地方。多年来,我在审计Ruby代码时一直引用此处提到的资源,偶尔会获得新的工具。果然,就在我准备写这篇文章时,Doyensec的Leonardo Giovannini于2024年10月16日发布了另一个针对Ruby 3.4的gadget。然后,如故事开头所述,Luke Jahnke于2024年11月24日带着"Ruby 3.4 Universal RCE Deserialization Gadget Chain"重新登场,并于12月3日再次发布"Gem::SafeMarshal escape"。这两种技术最终都被修补了。车轮在转动。

未来:结束Marshal疯狂

那么我们从这里去哪里?不幸的是,这个循环今天仍在继续。在我们最近为Ruby Central对RubyGems.org进行的安全审计中,我们发现了多个与Marshal相关的漏洞,这些漏洞存在于数千名开发人员日常依赖的代码中。由于难以利用,这两个漏洞都被评为信息性严重程度,但正如我们在本文中看到的,它们仍然是滴答作响的定时炸弹。其中一个漏洞启用了我们一次又一次看到的精确Gem模块gadget链。这些问题在像RubyGems.org这样具有安全意识的代码库中持续存在,充分说明了Marshal的使用在Ruby生态系统中根深蒂固的程度。

为了改善这种情况,我们向Ruby生态系统提出以下建议。

以下是Ruby开发人员应该做的:

  • 审计代码库中的Marshal使用:搜索Marshal.loadmarshal_load和类似方法。
  • 考虑使用我们的Ruby反序列化Semgrep规则rails-cache-store-marshalmarshal-load-methodjson-create-deserializationyaml-unsafe-load
  • 用更安全的替代方案替换Marshal
    • 使用YAML的safe_load和明确允许的类。
    • 使用JSON和手动对象构造。
    • 使用正确类型的数据库列,而不是不透明的二进制blob。
    • 考虑其他序列化格式,如MessagePack或Protocol Buffers。
  • 将反序列化添加到安全审查清单中,用于代码和依赖项审查。

对于Ruby核心团队和社区,需要更根本性的改变。我们建议分阶段逐步弃用并最终移除Marshal模块:

  1. 引入类似于YAML的Marshal.safe_load方法,默认仅反序列化基本类型。此方法应接受permitted_classes关键字参数,允许(反)序列化其他类。
  2. 调用Marshal.load时添加运行时警告。
  3. 在未来的Ruby版本中,使Marshal.load的行为类似于safe_load,并添加指向原始load行为的Marshal.unsafe_load方法。
  4. 最后,经过几个版本后,完全弃用并移除不安全行为。

像Python、Java和Ruby这样的语言几十年来一直受到此类bug的困扰。确实,在JSON和YAML等格式中还存在其他反序列化bug的可能性,但我们必须从某个地方开始。在2025年,使用Marshal实在是太危险了。同样的论点也可以用于Python和pickle,但我们将改天再讨论AI行业。Go和Rust没有这些类型的bug是有原因的。如果我们移除Marshal和这些序列化格式的不安全变体,那么这些bug就会消失。如果它们太符合人体工程学且太诱人使用,那么我们将继续看到这些bug。就这么简单。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计