diff --git a/src/pentesting-web/deserialization/README.md b/src/pentesting-web/deserialization/README.md index 99e46f67273..e89acdd71af 100644 --- a/src/pentesting-web/deserialization/README.md +++ b/src/pentesting-web/deserialization/README.md @@ -1174,6 +1174,38 @@ Using the arbitrary file write vulnerability, the attacker writes the crafted ca Treat any path where untrusted bytes reach `Marshal.load`/`marshal_load` as an RCE sink. Marshal reconstructs arbitrary object graphs and triggers library/gem callbacks during materialization.[[35]](#references) + +#### Ruby 3.3–4.0 universal RubyGems chain + +A dependency-free chain demonstrated on Ruby `4.0.6` (and reported to work unchanged on `3.3` through `4.0.6`) uses RubyGems code shipped with Ruby. The exact chain needs outbound HTTPS access and a writable destination such as `/tmp`; command execution has the Ruby process privileges and is **not** a local privilege escalation.[[53]](#references) + +The serialized graph is ordered so that the download/write side effect completes before a later `Hash`-key callback evaluates the downloaded Ruby source:[[53]](#references) + +1. Resolving `Gem::SpecFetcher` near the start of `Marshal.load` activates RubyGems autoloading and makes later gadget classes available even in a bare process. +2. A forged `Time` stores a `Gem::URI::Generic` object as its zone. During `Time._load`, `StringValueCStr` calls `to_str`; `Gem::URI::Generic#to_str` aliases `to_s`, which calls `@port.to_s` on an embedded `Gem::RequestSet::Lockfile` downloader. +3. The downloader uses an `s3` URI to reach signing code that emits an HTTPS URL. Traversal components placed in the URI's attacker-controlled port-derived path escape the RubyGems cache, while `Gem::Util.inflate` converts the remote `.rz` response into `/tmp/quick/Marshal.4.8/name-.gemspec`. +4. RubyGems then tries to parse that source file as serialized specification data and raises. This is useful because the file write already happened, and `time_mload` validates the zone inside `rb_rescue`, suppressing the post-write exception so graph reconstruction continues. +5. A `Gem::StubSpecification` with `@loaded_from` set to the written path is restored as a `Hash` key. Hash reconstruction invokes its `hash` method, which reaches `Gem::Specification.load(@loaded_from)`; that method reads the file and evaluates it as Ruby. + +The hosted response must be a deflated Ruby program. Make its last expression a valid `Gem::Specification` so the subsequent `name`, `version`, and `platform` accesses succeed and `Marshal.load` can return without the otherwise expected warning/exception:[[53]](#references) + +```bash +cat > payload.rb <<'RUBY' +puts `id` +Gem::Specification.new do |s| + s.name = "poc" + s.version = "1.0.0" +end +RUBY +ruby -rrubygems -e 'File.binwrite("poc-id.rz", Gem.deflate(File.binread("payload.rb")))' +``` + +`Marshal.dump` cannot normally produce a `Time` with an arbitrary object as its zone because `Time#_dump` serializes native state. The generator therefore dumps a placeholder object and replaces its bytes with a `TYPE_USERDEF` (`u`) `Time` entry wrapped by `TYPE_IVAR` (`I`), leaving the already serialized zone gadget after it. Keep the same number and order of symbol definitions on both sides of the replacement because later `TYPE_SYMLINK` entries are positional. Also stub `Gem::StubSpecification#hash` while building the generator-side `Hash`; otherwise the key gadget fires before serialization.[[53]](#references) + +This chain also shows why partially broken gadgets should be retested rather than discarded: RubyGems type checking removed the older `Gem::Version` `to_s` wrapper, and moving Git executable names out of attacker-restorable instance variables removed the older execution sink, but the surviving fetch/write primitive was reusable behind new trigger and evaluation gadgets.[[53]](#references) + +Specific hunting indicators include Marshal blobs naming `Gem::SpecFetcher`, `Time`, `Gem::URI::Generic`, `Gem::RequestSet::Lockfile`, and `Gem::StubSpecification`; HTTPS egress during deserialization; unexpected files below `/tmp/quick/Marshal.4.8/`; or stack traces containing `Time._load`, `Gem::StubSpecification#hash`, and `Gem::Specification.load`.[[53]](#references) + `Gem::SafeMarshal` was introduced to deserialize a restricted set of RubyGems objects, but the exact version still matters. Research against the Ruby 3.4 prerelease implementation showed that allowlisted `Date` and `Time` object paths could escape the restrictions and re-enter unrestricted `Marshal.load`; consequently, `SafeMarshal` is defense in depth, not a substitute for preventing attacker-controlled bytes from reaching a deserializer.[[47]](#references)[[48]](#references) The RubyGems 3.6.2 integration was merged into Ruby immediately before the Ruby 3.4 final release.[[49]](#references) @@ -1266,5 +1298,6 @@ Industrialized gadget discovery: - [50] [NCC Group – Freddy](https://github.com/nccgroup/freddy) - [51] [es.slideshare.net - Java Deserialization Vulnerabilities The Forgotten Bug Class](https://es.slideshare.net/codewhitesec/java-deserialization-vulnerabilities-the-forgotten-bug-class?next_slideshow=1) - [52] [alphabot.com - Fastjson Exceptional Deserialization Vulnerabilities](https://www.alphabot.com/security/blog/2020/java/Fastjson-exceptional-deserialization-vulnerabilities.html) +- [53] [elttam – Ruby 4.0 Universal RCE Deserialization Gadget Chain](https://elttam.com/blog/ruby-4-0-universal-rce-deserialization-gadget-chain) {{#include ../../banners/hacktricks-training.md}}