セキュリティ
Ruby 4.0 Universal RCE Deserialization Gadget Chain
Ruby 4.0 Universal RCE Deserialization Gadget Chain (elttam.com)
要約
Ruby 4.0.6 (執筆時点での最新リリース) に対して、単一の Marshal.load からコマンド実行を可能にする新しいユニバーサルなガジェットチェーンが発見されました。このチェーンは、Ruby 3.3 以降でも変更なく動作します。以前のチェーンを破った RubyGems のコミットを回避し、新しいガジェットや再利用されたガジェットを組み合わせて構築されています。
全文翻訳
メインコンテンツへスキップ
重要なお知らせバナー閉じる
お知らせバナー
Luke Jahnke 作
2026年8月14日
Ruby 4.0 Universal RCE Deserialization Gadget Chain
Ruby 4.0.6 (執筆時点での最新リリース) でコマンド実行を可能にする新しいユニバーサルチェーン。Marshal.load 1 回で実現します。
webrubydeserialisation
このページについて
TOC 要素
共有:
はじめに
2026年8月5日、OpenAI は評価中の AI エージェントの集団がサンドボックスを突破し、実行されていたクラスターの管理者権限を奪取したことを明らかにしました。その一部は、Ruby のデシリアライゼーションを悪用してコマンドを実行することによって達成されました。これは私たちの注意を引きました。なぜなら、2018 年に私たちは、依存関係なしで標準ライブラリのみで構築された、Ruby 用の最初のユニバーサル RCE デシリアライゼーションガジェットチェーンを発表したからです。そのチェーンは Ruby バージョン 2.6.10 までしか動作せず、最も最近の公開チェーンは 3.4-rc までしか動作しません。
この投稿では、執筆時点での最新リリースである Ruby 4.0.6 で単一の Marshal.load からコマンド実行を可能にする新しいユニバーサルチェーンを発表します。これは 3.3 まで変更なく動作します。このチェーンは、未使用のソースからの新しいガジェットと、新しい用途に置かれた古いガジェットで構築されています。
背景
シリアライゼーションとは、オブジェクトをバイトのシーケンスに変換するプロセスであり、ネットワーク経由で転送したり、ファイルシステムやデータベースに保存したりできます。これらのバイトには、元のオブジェクトを再構築するために必要なすべての情報が含まれています。この再構築プロセスをデシリアライゼーションと呼びます。
各プログラミング言語は通常、独自のネイティブシリアライゼーション形式を持ち、このプロセスをシリアライゼーション/デシリアライゼーション以外の名前で参照する場合があります。Ruby の場合、マーシャリングとアンマーシャリングという用語が一般的に使用され、操作は Marshal.dump と Marshal.load によって提供されます。
Ruby デシリアライゼーションの 13 年間
ユニバーサル Ruby デシリアライゼーションガジェットチェーンは 2018 年に始まり、Ruby on Rails に対するアプリケーション固有のチェーンに関する以前の研究に基づいて構築され、そのユニバーサルな作業は、その後に続いたアプリケーション固有のチェーンにフィードバックされました。以下のマイルストーンのいくつかは、このチェーンが構築されるピースを提供します。
2013 年 1 月 10 日 - Hailey Somerville による Rails 3.2.10 リモートコード実行
2013 年 1 月 31 日 - Hailey Somerville による Ruby バグトラッカーの問題
2016 年 5 月 6 日 - Phenoelit の joernchen による Ruby on Rails アプリケーションへの攻撃
2018 年 11 月 8 日 - Luke Jahnke (elttam) による Ruby 2.x ユニバーサル RCE デシリアライゼーションガジェットチェーン
2019 年 1 月 2 日 - ooooooo_q による CVE-2019-5420
2019 年 3 月 2 日 - Etienne Stalmans による Ruby YAML.load を使用したユニバーサル RCE
2019 年 6 月 20 日 - Sivathmican Sivakumaran および Pengsu Cheng (Trend Micro Security Research Team) による Ruby on Rails Active Storage 不安全なデシリアライゼーション経由のリモートコード実行
2021 年 1 月 7 日 - William Bowling による Ruby 2.x-3.x 用ユニバーサルデシリアライゼーションガジェット
2021 年 1 月 9 日 - Etienne Stalmans による Ruby YAML.load (バージョン > 2.7) を使用したユニバーサル RCE
2022 年 3 月 28 日 - Ruby デシリアライゼーション - Rails 上のガジェット (httpvoid)
2022 年 4 月 4 日 - Round Two: William Bowling による Ruby 2.x-3.x 用更新ユニバーサルデシリアライゼーションガジェット
2022 年 5 月 17 日 - Ben Lincoln (Bishop Fox) による Ruby の脆弱性: 危険な Open, Send, およびデシリアライゼーション操作のエクスプロイト
2024 年 3 月 13 日 - Alex Leahu (Include Security) による Rubyland でのデシリアライゼーションガジェットチェーンの発見
2024 年 6 月 20 日 - Peter Stöckli (GitHub) による JSON 送信によるコマンド実行?Ruby プロジェクトでの安全でないデシリアライゼーションの脆弱性がどのように機能するかを学ぶ
2024 年 10 月 17 日 - Leonardo Giovannini (Doyensec) による Marshal ロード用の更新された Ruby ガジェット
2024 年 11 月 24 日 - Luke Jahnke による Ruby 3.4 ユニバーサル RCE デシリアライゼーションガジェットチェーン
2024 年 12 月 3 日 - Luke Jahnke による Gem::SafeMarshal エスケープ
2025 年 8 月 20 日 - Matt Schwager (Trail of Bits) による Marshal madness: Ruby デシリアライゼーションエクスプロイトの簡単な歴史
2026 年 8 月 5 日 - OpenAI による Black Hat USA 2026 で開示された、自律 AI エージェントによる Ruby (JRuby) デシリアライゼーションのインザワイルドでのエクスプロイトの開示
2026 年 - Luke Jahnke (本投稿) による Ruby 4.0 ユニバーサル RCE デシリアライゼーションガジェットチェーン
3.4 チェーンが破られた方法
2024 年後半に公開された最新の公開チェーンは、このペイロードで 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) ] )
公開から 10 日後、RubyGems に 2 つのコミットが入り、それらが依存していたガジェットが削除されました。どちらもこの解説を動機として引用しています。両方とも Ruby 3.4.0 で出荷されたため、チェーンはリリース候補では機能しますが、リリース版では機能しません。
最初のコミット 62b49465f8 は、「marshal_load メソッドでの型チェックの改善」と題されており、「これらのクラスをガジェットとして使用することをより困難にする」と述べています。
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
2 番目のコミット 89ad04db86 は、「インスタンス変数に実行可能ファイル名を格納するのを停止する」と題されており、「ACE ガジェットとしてのこれらのクラスの使用を削除する」と述べています。
Gem::Source::Git および Gem::Resolver::GitSet は、git 実行可能ファイル名をインスタンス変数に格納していましたが、Marshal はそれを直接復元し、後にプロセススポーンに渡されました。
- @git = ENV["git"] || "git"
名前は、使用時に環境から読み取られるようになったため、設定するインスタンス変数は残っていません。
これらの 2 つのコミットは to_s_wrapper と exec_gadget を破りましたが、Gem::SpecFetcher と call_url_and_create_folder はそのまま残され、Ruby 4.0 で動作します。
新しいチェーンの構築
利用可能なガジェットセットの拡張
チェーンは Gem::SpecFetcher で始まりますが、クラスが何らかの作業を行うからではなく、Marshal.load が定数を解決する必要があり、それを解決すると RubyGems のオートロードがトリガーされ、定義しているファイルが要求され、それがさらに他のファイルを要求するためです。したがって、空の Ruby プロセスは、チェーンによって到達可能な少数のクラスで開始され、単一の定数参照の後、ガジェットを選択するためのより大きなセットで終了します。これには、チェーンの残りの部分が依存する 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
...
end
end
このメソッドはディスクからファイルを読み込み、その内容を直接 eval に渡すため、Gem::Specification.load に渡されるファイル名とファイルの内容の両方を制御できるチェーンは、最終的に任意のコード実行につながります。
load メソッドの呼び出し
利用可能なセットは、@controlled.load(@also_controlled) のような柔軟なガジェットを提供しませんが、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 メソッド呼び出しのトリガー
Ruby は、オブジェクトが Hash のキーとして使用されるときに、そのオブジェクトに対して hash を呼び出します。Marshal.load はキーを挿入することによってハッシュを再構築するため、作成された Gem::StubSpecification をペイロードのどこかにキーとして配置するだけで、hash が呼び出されます。
Java 愛好家ならこれを認識するでしょう。HashMap.readObject は、復元するすべてのキーに対して hashCode を呼び出しますが、これは ysoserial のチェーンの大部分のエントリポイントです。
トリガーは、メンテナーが...