Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/pentesting-web/deserialization/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<sup>[[35]](#references)</sup>


#### 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.<sup>[[53]](#references)</sup>

The serialized graph is ordered so that the download/write side effect completes before a later `Hash`-key callback evaluates the downloaded Ruby source:<sup>[[53]](#references)</sup>

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:<sup>[[53]](#references)</sup>

```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.<sup>[[53]](#references)</sup>

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.<sup>[[53]](#references)</sup>

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`.<sup>[[53]](#references)</sup>

`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.<sup>[[47]](#references)</sup><sup>[[48]](#references)</sup> The RubyGems 3.6.2 integration was merged into Ruby immediately before the Ruby 3.4 final release.<sup>[[49]](#references)</sup>


Expand Down Expand Up @@ -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}}