// Ruby für Pentester · Teil 6
Einen Nicht-Standard-Hash knacken
Der Admin-Hash liegt in keinem Standardformat vor, das hashcat mitbringt — also bilden wir OFBiz' genaues Hashing in Ruby nach und bruten es gegen eine Wortliste. Dann bauen wir einen Thread-Pool, um es zu parallelisieren — und treffen die GVL, den Grund, warum Teil 4's Threads Netzwerkarbeit beschleunigten, reines Hashing aber nicht.
Teil 5 hat $SHA$d$uP0_QaVBpDWFeo8-dRzDqRwXQ2I von der Box geholt. Das ist kein
bcrypt, kein bloßes SHA1 — es ist OFBiz’ eigenes Schema, also hat hashcat von
Haus aus keinen Modus dafür. Wenn ein Hash-Format selbstgebaut ist, ist der Zug,
es nachzubauen — in ein paar Zeilen — und selbst zu knacken. Das hier lehrt
Rubys Digest- und Base64-Bibliotheken — und dann Thread-Pools und die Decke,
die die GVL ihnen aufsetzt.
Das Format lesen
Der String besteht aus drei Feldern, verbunden durch $:
$SHA$ d uP0_QaVBpDWFeo8-dRzDqRwXQ2I
algo salt digest
Das Rezept ist also SHA1(salt + password), der rohe Digest base64-kodiert
mit dem URL-sicheren Alphabet (- und _ statt + und /) und ohne
=-Padding. Zerlege das Ziel, um die Teile zu bekommen:
TARGET = '$SHA$d$uP0_QaVBpDWFeo8-dRzDqRwXQ2I'
_, _, salt, digest = TARGET.split('$') # ['', 'SHA', 'd', 'uP0_...']
split('$') liefert vier Teile — den leeren String vor dem ersten $, dann
SHA, das Salt und den Digest. Uns interessieren nur die letzten zwei, also
landen die ersten zwei in _ (Rubys Wegwerf-Name).
Aus dem Quellcode lesen
Den String zu parsen verrät dir die Form; der Quellcode der Anwendung
verrät dir das genaue Rezept, ohne Raten. OFBiz ist quelloffen, also ist sein
HashCrypt.getCryptedBytes eine Suche entfernt:
messagedigest.update(salt.getBytes(UTF_8));
messagedigest.update(passwordBytes);
return Base64.encodeBase64URLSafeString(messagedigest.digest());
Das ist die ganze Spezifikation — base64url(SHA1(salt + password)), Salt
zuerst. Bei einem Closed-Source-Ziel gilt derselbe Zug: Zieh die Routine aus
einem dekompilierten Binary, einem JS-Bundle oder der Hersteller-Doku. Die echte
Konstruktion zu lesen schlägt das Raten von hashcat-Modi — du reproduzierst
einen Hash und weißt, dass deine Umsetzung stimmt, bevor du einen ganzen
Crack-Lauf startest.
Den Hash nachbauen
Zwei Teile der Standardbibliothek erledigen die Arbeit: Digest::SHA1 für den
Hash und Base64 für die Kodierung.
require 'digest'
require 'base64'
def encode(word, salt)
Base64.urlsafe_encode64(Digest::SHA1.digest(salt + word), padding: false)
end
Digest::SHA1.digest(str)gibt die rohen 20 Bytes des Hashes zurück (nimm.hexdigest, wenn du stattdessen Hex willst).Base64.urlsafe_encode64(bytes, padding: false)kodiert diese Bytes mit dem URL-sicheren Alphabet und lässt das=-Padding weg — genau OFBiz’ Format.
Wir können beweisen, dass die Umsetzung stimmt, sobald wir das Passwort finden:
Seine encode-Ausgabe wird gleich dem gespeicherten digest sein.
Der sequentielle Crack
Jetzt eine Wortliste hindurchbruten. Streame die Datei Zeile für Zeile —
File.foreach hast du in Teil 5 kennengelernt — und halte beim ersten Treffer:
File.foreach('rockyou.txt', chomp: true) do |word|
if encode(word, salt) == digest
puts "[+] Password: #{word}"
break
end
end
chomp: true streift den abschließenden Zeilenumbruch von jeder Zeile, damit er
den Hash nicht vergiftet. Lauf es, und das Passwort fällt heraus:
$ ruby crack.rb
[+] Password: monkeybizness
Parallelisieren — und die GVL
rockyou.txt hat vierzehn Millionen Zeilen. Der naheliegende Speedup ist „wirf
Teil 4’s Threads darauf” — teile die Arbeit auf einen Pool von Arbeitern auf. Der
idiomatische Weg ist eine thread-sichere Queue, die die Threads leeren:
queue = Queue.new
File.foreach('rockyou.txt', chomp: true) { |word| queue << word }
found = nil
workers = 8.times.map do
Thread.new do
until found || queue.empty?
word = queue.pop(true) rescue break
found = word if encode(word, salt) == digest
end
end
end
workers.each(&:join)
puts found ? "[+] Password: #{found}" : '[-] Not found'
Queueist Rubys thread-sichere FIFO — viele Threads können darauspopen, ohne sie zu beschädigen, also ist sie der natürliche Weg, einen Pool zu füttern.queue.pop(true)popt im nicht-blockierenden Modus; wenn die Queue leerläuft, wirft sie, undrescue breakverabschiedet diesen Arbeiter sauber.Thread#joinwartet, bis jeder Arbeiter fertig ist, bevor wirfoundlesen.
Nun der Haken, und er ist die eigentliche Lektion: Auf Standard-Ruby (MRI) ist das für reines Hashing nicht wirklich schneller. Ruby hat einen Global VM Lock (GVL) — nur ein Thread führt zur gleichen Zeit Ruby-Code aus. In Teil 4 halfen Threads trotzdem, weil ein Thread, der am Netzwerk blockiert, die GVL freigibt, während er wartet. Hashing gibt nichts frei; es ist reine CPU, also wechseln sich die acht Arbeiter nur auf einem Kern ab.
Glaub mir nicht — miss es. Rubys benchmark-Bibliothek stoppt die Zeit von
Blöcken; lauf das Hashing sequentiell, dann über acht Threads verteilt:
require 'benchmark'
words = File.readlines('rockyou.txt', chomp: true).first(2_000_000)
Benchmark.bm(12) do |bm|
bm.report('sequential') { words.each { |w| encode(w, salt) } }
bm.report('8 threads') do
words.each_slice(words.size / 8).map do |slice|
Thread.new { slice.each { |w| encode(w, salt) } }
end.each(&:join)
end
end
Die real-Spalte — die tatsächliche Uhrzeit von Anfang bis Ende — erzählt die
Geschichte:
user system total real
sequential 4.960000 0.010000 4.970000 ( 4.963283)
8 threads 5.260000 0.030000 5.290000 ( 5.270911)
Acht Threads brauchten etwas länger, nicht achtmal kürzer: Die GVL ließ sie sich abwechseln, und wir zahlten ein wenig Overhead für das Vergnügen. Das ist die Lektion in Zahlen.
Warum also das Muster lernen? Weil es genau richtig ist und sich auszahlt,
sobald die Arbeit nicht reine Ruby-CPU ist: I/O-gebundene Schleifen wie Teil 4,
C-Erweiterungen, die die GVL freigeben, oder eine GVL-freie Laufzeit wie JRuby
oder TruffleRuby, wo dieselben Threads wirklich parallel laufen. Für echte
Mehrkern-CPU-Arbeit auf MRI greifst du an Threads vorbei — zu getrennten
Prozessen (Process.fork) oder Ractors —, aber das ist ein schwereres
Werkzeug, als ein Wortlisten-Crack braucht.
Erkenntnisse
- Selbstgebaute Hash-Formate (hier
$SHA$salt$digest) sind meist ein kurzes Rezept — bau sie mitDigest+Base64nach, statt nach einem Tool-Modus zu suchen. - Der Quellcode der App ist die maßgebliche Spezifikation für sein Hashing —
lies
HashCrypt(oder eine dekompilierte Routine), statt hashcat-Modi zu raten. Dieselbe Lesen-dann-Nachbauen-Schleife knackt Eigenbau-Schemata in jedem Stack. Digest::SHA1.digestgibt rohe Bytes zurück (.hexdigestfür Hex);Base64.urlsafe_encode64(bytes, padding: false)trifft OFBiz’ URL-sichere, padding-lose Kodierung.- Eine
Queueist Rubys thread-sicherer Weg, einen Pool vonThreads zu füttern;pop(true)+rescue breakist das Standardmuster „leeren, bis leer”. - Die GVL bedeutet, dass Threads nur I/O-gebundene Arbeit beschleunigen (Teil 4), nicht CPU-gebundene wie Hashing — dafür greifst du zu Prozessen oder einer GVL-freien Laufzeit.
- Die
benchmark-Bibliothek (Benchmark.bm) stoppt die Zeit von Blöcken; diereal-Spalte ist die Uhrzeit von Anfang bis Ende — der ehrliche Weg zu prüfen, ob eine Optimierung geholfen hat.
Übungen
- Pack das Hashing in
ofbiz_hash(word, salt)und stell sicher, dass es das bekannte Paar reproduziert —ofbiz_hash('monkeybizness', 'd')sollte dem gespeicherten Digest gleichen. - Gib Fortschritt aus dem Cracker aus: alle 100.000 Wörter die Anzahl drucken
(
i % 100_000 == 0). - Lauf den Benchmark erneut mit 1, 2, 4 und 8 Threads und vergleich die
real-Spalte. Sieh zu, wie sie flach bleibt — das ist die GVL, von deiner eigenen Hand gemessen.
Weiter — Teil 7: Dieses Passwort gehört auch root. Wir setzen es zum
Eskalieren ein — und lernen, wie Ruby externe Programme ausführt, von system
bis zu einem PTY-gesteuerten su.