// 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.
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/debugist 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/statsist 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_mapmappt und flacht ab — ideal, um aus einem Hash vonpath => [methods]eine flache Liste von[method, path]-Paaren zu machen.%w[a b c]baut günstig ein String-Array;eachiteriert;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_generatesichern strukturierte Daten, die du später wieder lädst.
Übungen
- Die Wortliste verpasst POST-only-Routen wie
/api/orders(ein GET liefert 404). Erweiteregetzu einemprobe(method, path), das beides senden kann, und lass Discovery erneut laufen, sodass auch POST-Endpunkte auftauchen. - 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.
- 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.