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

Die API kartieren

Eine Spezifikation sagt dir, was eine API dich sehen lassen will. Wir parsen sie zu einem Inventar und proben dann eine Wortliste, um die ausgelassenen Endpunkte zu finden — und lernen dabei Arrays, Hashes, Iteration und den Unterschied zwischen einem 404 und einem 401.

RubyReconArraysHashes
5 Min. Lesezeit

Teil 1 ließ uns einloggen und ein Profil lesen. Bevor du Bugs jagst, brauchst du eine Karte: jeden Endpunkt, den die API freilegt, nicht nur die, für die sie wirbt. Dieser Teil baut diese Karte auf zwei Wegen — aus der veröffentlichten Spezifikation und dann durch direktes Proben des Servers — und bringt dir dabei Rubys zentrale Sammlungswerkzeuge bei.

Beginne eine neue Datei, map.rb, mit einem winzigen GET-Client. Net::HTTP.get_response erledigt die ganze Anfrage in einem Aufruf, mehr brauchen wir für Recon nicht:

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

BASE = 'http://127.0.0.1:4567'

def get(path)
  Net::HTTP.get_response(URI("#{BASE}#{path}"))
end

Die Spezifikation lesen

Die meisten APIs liefern eine maschinenlesbare Spezifikation — OpenAPI/Swagger —, die ihre Endpunkte auflistet. Unsere serviert eine unter /openapi.json. Hol sie und mach daraus eine flache Liste von [method, path]-Paaren:

spec = JSON.parse(get('/openapi.json').body)

documented = spec['paths'].flat_map do |path, methods|
  methods.map { |m| [m.upcase, path] }
end

documented.each do |method, path|
  puts "#{method.ljust(4)} #{path}"
end

spec['paths'] ist ein Hash aus path => [methods]. flat_map führt den Block für jedes Paar aus und flacht die Ergebnisse zu einer Liste ab — ohne es bekämst du ein Array aus Arrays. Darin verwandelt methods.map { |m| [m.upcase, path] } jeden Methoden-String in ein [method, path]-Paar. ljust(4) füllt die Methode auf, damit die Pfade bündig stehen. Führe es aus:

$ ruby map.rb
POST /login
GET  /api/me
GET  /api/users/{id}
GET  /api/markets
GET  /api/markets/{id}
POST /api/orders
GET  /api/search
POST /api/redeem

Acht dokumentierte Endpunkte. Das ist, was die API zugibt.

Nach dem Rest proben

Spezifikationen lügen durch Auslassung. Debug-Routen, Admin-Panels und vergessene Endpunkte schaffen es selten in die veröffentlichten Pfade — genau deshalb lohnt es sich, sie zu finden. Die Technik heißt Content Discovery: nimm eine Wortliste wahrscheinlicher Pfade und frage den Server zu jedem einzelnen. Füge eine Liste und eine Schleife hinzu:

WORDLIST = %w[
  api/me api/users/1 api/markets api/orders api/admin
  api/admin/stats api/admin/users api/debug api/config
  api/redeem api/search api/health backup .git/config
]

WORDLIST.each do |word|
  code = get("/#{word}").code.to_i
  next if code == 404
  puts "#{code}  /#{word}"
end

%w[...] ist die Kurzform für ein Array von Strings — keine Anführungszeichen oder Kommas nötig. next if code == 404 überspringt den Rest des Schleifenkörpers für diese Iteration, sodass wir nur existierende Pfade ausgeben. Führe es aus:

$ ruby map.rb
...
401  /api/me
401  /api/users/1
200  /api/markets
401  /api/admin/stats
200  /api/debug
200  /api/search

Lies diese Statuscodes genau — sie sind der ganze Punkt:

  • 404 heißt hier ist nichts; die haben wir übersprungen.
  • 200 heißt hier und offen. /api/debug ist ein 200 und steht nicht in der Spezifikation. Das ist der Fund: ein undokumentierter Endpunkt, der frei antwortet.
  • 401 heißt hier, aber braucht Auth. Ein 401 ist keine Sackgasse — es ist ein bestätigter Endpunkt, für den du nur ein Token brauchst. /api/admin/stats ist ein 401 und fehlt ebenfalls in der Spezifikation: eine Admin-Route, die jemand zu dokumentieren vergaß.

Drei davon — /api/me, /api/users/1, /api/admin/stats — sind 401, weil get kein Token sendet; jeder ist ein echter Endpunkt, der auf eines wartet, keine Wand. Ein Jäger, der jedes Nicht-200 als „nichts“ behandelt, läuft direkt am Admin-Panel vorbei.

Die Karte speichern

Spätere Teile laufen erneut gegen diese API, und in Teil 7 diffen wir einen Lauf gegen den nächsten. Also schreib das Inventar als JSON auf die Platte:

inventory = documented.map { |method, path| { 'method' => method, 'path' => path } }
File.write('inventory.json', JSON.pretty_generate(inventory))
puts "[+] wrote #{inventory.size} endpoints to inventory.json"

JSON.pretty_generate serialisiert mit Einrückung, sodass die Datei lesbar ist, und File.write legt sie in einem Aufruf ab. Diese Datei ist die Basis, um die herum unser Scanner wächst.

Erkenntnisse

  • flat_map mappt und flacht ab — ideal, um aus einem Hash von path => [methods] eine flache Liste von [method, path]-Paaren zu machen.
  • %w[a b c] baut günstig ein String-Array; each iteriert; next if ... überspringt eine Iteration.
  • In der Recon sind Statuscodes Signal: 404 ist leer, 200 ist offen, 401 ist „existiert, braucht Auth“. Die undokumentierten 200er und 401er sind, wo die Bugs stecken.
  • File.write + JSON.pretty_generate sichern strukturierte Daten, die du später wieder lädst.

Übungen

  1. Die Wortliste verpasst POST-only-Routen wie /api/orders (ein GET liefert 404). Erweitere get zu einem probe(method, path), das beides senden kann, und lass Discovery erneut laufen, sodass auch POST-Endpunkte auftauchen.
  2. Gib nach der Discovery eine einzeilige Zusammenfassung aus: wie viele Pfade 200 gegen 401 zurückgaben, über zwei Zähler, die du in der Schleife erhöhst.
  3. Diffe die dokumentierten Pfade gegen das, was du entdeckt hast. Welcher Live-Endpunkt steht weder in der Liste der Spezifikation noch in einem 200/401, den du erwarten würdest? (Tipp: Er gibt mehr aus, als er sollte.)

Weiter — Teil 3: /api/users/{id} antwortete ohne Token mit 401 — aber was passiert mit einem? Wir richten unser eigenes Token auf die Datensätze anderer Benutzer und finden unsere erste echte Schwachstelle: gebrochene Objektebenen-Autorisierung.

← Alle Teile