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