// Ruby für Pentester · Teil 9

Dein Werkzeug testen

Ein Werkzeug, das du erneut ausführst, verdient ein Sicherheitsnetz. Wir lernen minitest kennen, Rubys eingebautes Test-Framework, und wie man testbaren Code schreibt — die Logik, die du prüfen kannst, vom Netzwerk trennen, das du nicht prüfen kannst.

RubyTestingminitestDesign
4 Min. Lesezeit

Teil 8 hat aus unseren Skripten ein echtes Werkzeug gemacht. Werkzeuge werden bearbeitet, und Bearbeitungen brechen Dinge — meist auf die stille Art, wo es noch läuft, aber Müll zurückgibt. Ein Test ist ein zweites Stück Code, das prüft, ob das erste noch tut, was du denkst. Einmal geschrieben, wird jede künftige Änderung gratis geprüft. Ruby liefert ein Test-Framework in der Standardbibliothek: minitest.

Dein erster Test

Eine Testdatei erfordert minitest/autorun, erbt von Minitest::Test und definiert Methoden, deren Namen mit test_ beginnen:

require 'minitest/autorun'

class SanityTest < Minitest::Test
  def test_math_still_works
    assert_equal 42, 40 + 2
  end
end

Lauf die Datei direkt, und minitest findet und führt jede test_-Methode aus:

$ ruby sanity_test.rb
Run options: --seed 12345

# Running:

.

Finished in 0.001s, 1000 runs/s, 1000 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Jeder . ist ein bestandener Test; assert_equal(expected, actual) ist das Arbeitspferd — es scheitert laut, wenn die beiden nicht übereinstimmen. Das ist die ganze Schleife: behaupte, was wahr sein soll, lauf, achte auf die Punkte.

Die Logik testen, nicht das Netzwerk

Die testenswerten Teile sind die mit echter Logik — das Hashing aus Teil 6, die Extraktions-Regex aus Teil 5. Entscheidend: keiner braucht eine lebende Box. Sie sind reine Funktionen von Eingabe zu Ausgabe, also kann ein Test sie in Millisekunden festnageln:

require 'minitest/autorun'
require 'digest'
require 'base64'

def ofbiz_hash(word, salt)
  Base64.urlsafe_encode64(Digest::SHA1.digest(salt + word), padding: false)
end

def parse_hash(body)
  body[/currentPassword="(\$SHA\$[^"]+)"/, 1]
end

class BiznessTest < Minitest::Test
  def test_hash_matches_known_pair
    assert_equal 'uP0_QaVBpDWFeo8-dRzDqRwXQ2I', ofbiz_hash('monkeybizness', 'd')
  end

  def test_wrong_password_differs
    refute_equal 'uP0_QaVBpDWFeo8-dRzDqRwXQ2I', ofbiz_hash('hunter2', 'd')
  end

  def test_parse_pulls_the_hash
    line = 'currentPassword="$SHA$d$uP0_QaVBpDWFeo8-dRzDqRwXQ2I" userLoginId="admin"'
    assert_equal '$SHA$d$uP0_QaVBpDWFeo8-dRzDqRwXQ2I', parse_hash(line)
  end
end
$ ruby test_bizness.rb
...
3 runs, 3 assertions, 0 failures, 0 errors, 0 skips

Drei wissenswerte Assertions: assert_equal (sie stimmen überein), refute_equal (sie stimmen nicht — beweist, dass ein falsches Passwort einen anderen Hash gibt) und assert_includes für „ist dieser Teilstring darin”. Es gibt eine für jede Form der Erwartung.

Auf Testbarkeit hin entwerfen

Beachte, warum parse_hash testbar war: Es nimmt einen Response-Body als String und gibt den Hash zurück — das Parsen ist vom Abrufen getrennt. Das ist der ganze Trick. Eine Methode, die die Anfrage macht und die Antwort durchwühlt, kann ohne ein lebendes Ziel nicht getestet werden; teil sie in zwei — eine Methode ruft ab, eine andere parst den zurückgegebenen String —, und der Parser wird isoliert prüfbar.

Tests zu schreiben schiebt dich leise zu dieser saubereren Form, was der halbe Grund ist, sich die Mühe zu machen. Das Werkzeug, das du testen kannst, ist meist das gut gebaute Werkzeug.

Erkenntnisse

  • minitest liegt Ruby bei: require 'minitest/autorun', erbe von Minitest::Test, und jede test_*-Methode läuft, wenn du die Datei ausführst.
  • assert_equal, refute_equal, assert_includes (und Vettern) drücken aus, was wahr sein soll; eine gescheiterte Assertion meldet genau, was nicht passte.
  • Teste reine Logik — Hashing, Parsen —, weil sie kein Netzwerk braucht und die Teile festnagelt, die bei einer Änderung am ehesten brechen.
  • Parsen vom Abrufen zu trennen macht Code testbar; nach Tests zu greifen stupst dich in dieses bessere Design.

Übungen

  1. Füge einen Test hinzu, dass ofbiz_hash deterministisch ist — dasselbe Wort und Salt erzeugen immer denselben Digest.
  2. Schreib parse_user(body), das userLoginId herauszieht, dann einen Test, der behauptet, dass es für die Beispielzeile 'admin' zurückgibt.
  3. Füge eine setup-Methode hinzu, die die Beispielzeile einmal in @line baut, und schreib die Parse-Tests so um, dass sie @line nutzen, statt den String zu wiederholen.

Weiter — Teil 10: das Finale. Wir nehmen genau diese Kette und packen sie neu als Metasploit-Modul — dasselbe Ruby, das du bereits kennst, eingelassen in die Struktur des Frameworks, sodass use und run einfach funktionieren.

← Alle Teile