Unsafe Deserialization in Ruby
Ruby uses the Marshal library to serialize and unserialize objects. For example, the following script creates an instance of the object User, serializes it, and then prints the string representation of the object.
User = Struct.new(:name, :role)
user = User.new('Mike', :admin)
puts Marshal.dump(user).inspect
# Prints the following string representation:
# "\x04\bS:\tUser\a:\tnameI\"\tMike\x06:\x06ET:\trole:\nadmin"
The string representation can then be deserialized again to recreate the object instance and access its attributes.
user = Marshal.load("\x04\bS:\tUser\a:\tnameI\"\tMike\x06:\x06ET:\trole:\nadmin")
print(user.name);
# It prints the following string:
# Mike
Vulnerable Example
The exploitation of deserialization in Ruby happens when user-controlled input is passed as the first argument of the Marshal.load() function.
To be exploitable, the vulnerable piece of code must have enough Ruby code in scope to build a gadget chain, which means a chain of reusable code that causes a meaningful impact when invoked.
For example, assume that Marshal.load() deserializes user-provided data. An attacker could craft a malicious payload like the following one, which abuses an existing class to execute a command when deserialized.
class FSResource
def initialize path
@path = path
end
def to_s
open(@path).read
end
end
# Craft the payload to execute `id` via the `open` function instead of opening a file
obj = FSResource.new('|id')
payload = Marshal.dump(obj)
# Unserializing the payload allows to execute arbitrary commands
serialized_obj = Marshal.load(payload)
puts serialized_obj
# It prints the output of id:
# uid=1002(admin) gid=1002(admin) groups=1002(admin)
A number of real code chains against Ruby and Ruby on Rails have been discovered and published by security researchers in the past.
References
ZDI - Remote Code Execution via Ruby on Rails Active Storage Insecure Deserialization
ELTTAM - Ruby 2.x Universal RCE Deserialization Gadgets Chain