// Bug-Bounty-Automatisierung in Ruby · Teil 7
Über Läufe hinweg diffen
Der beste Zeitpunkt, einen Bug zu finden, ist genau dann, wenn er ausgeliefert wird. Wir machen Schnappschüsse der API-Oberfläche auf die Platte und diffen jeden Lauf gegen den letzten, sodass ein neuer Endpunkt, ein neues Feld oder ein geänderter Statuscode von selbst auftaucht — das „Was hat sich geändert“-Signal, mit dem ein Bounty-Bot läuft. Dateien, Sets und Hash-Vergleich.
Alles bisher war ein Einmal-Probe. Ein Bot, der jede Stunde läuft, hat eine Superkraft, die ein Mensch nicht hat: Gedächtnis. Er kann sich genau merken, wie die API letztes Mal aussah, und alles Neue markieren — einen frischen Endpunkt, ein zusätzliches Feld in einer Antwort, eine Route, die plötzlich keine Auth mehr verlangt. Neuer Code ist, wo neue Bugs leben, und ein Diff ist, wie du ihn am Tag der Auslieferung fängst.
Das bauen wir: die API fingerprinten, speichern und beim nächsten Lauf gegen die
gespeicherte Kopie vergleichen. Neue Datei, diff.rb:
require 'net/http'
require 'json'
require 'uri'
BASE = 'http://127.0.0.1:4567'
SNAPSHOT = 'snapshot.json'
def get(path)
Net::HTTP.get_response(URI("#{BASE}#{path}"))
end
Die Oberfläche fingerprinten
Wir wollen keine ganzen Antworten speichern — Werte ändern sich ständig und wir würden im Rauschen ertrinken. Wir wollen die Form: für jeden Endpunkt seinen Statuscode und die Menge der Schlüssel, die er zurückgibt. Das ist stabil, wenn sich nichts Strukturelles ändert, und es bewegt sich in dem Moment, in dem die API ein Feld oder eine Route dazubekommt:
ENDPOINTS = %w[/api/markets /api/markets/1 /api/debug /openapi.json]
def fingerprint
ENDPOINTS.each_with_object({}) do |path, snap|
res = get(path)
body = JSON.parse(res.body) rescue nil
keys = body.is_a?(Hash) ? body.keys.sort : []
snap[path] = { 'status' => res.code.to_i, 'keys' => keys }
end
end
each_with_object({}) läuft die Endpunkte ab, während es einen Hash aufbaut —
snap beginnt leer, wir füllen ihn, dann wird er am Ende zurückgegeben. Für
jeden Pfad erfassen wir den Status und die sortierten Top-Level-Schlüssel.
rescue nil bedeutet, dass ein Nicht-JSON-Körper einfach eine leere
Schlüsselliste ergibt, statt abzustürzen.
Speichern, dann vergleichen
Jetzt das Gedächtnis. Wenn es noch keinen Schnappschuss gibt, ist dieser Lauf die Basis — speichern und stoppen. Wenn es einen gibt, lade ihn und vergleiche, Pfad für Pfad:
current = fingerprint
if File.exist?(SNAPSHOT)
previous = JSON.parse(File.read(SNAPSHOT))
changes = 0
(previous.keys | current.keys).sort.each do |path|
was = previous[path]
now = current[path]
if was.nil?
puts " + NEW #{path}"
changes += 1
elsif now.nil?
puts " - REMOVED #{path}"
changes += 1
elsif was != now
gained = now['keys'] - was['keys']
note = gained.empty? ? '' : " new keys #{gained.inspect}"
puts " ~ CHANGED #{path} status #{was['status']}->#{now['status']}#{note}"
changes += 1
end
end
puts changes.zero? ? '[*] no changes since last run' : "[!] #{changes} change(s) — look here"
else
puts '[*] no snapshot yet — saving baseline'
end
File.write(SNAPSHOT, JSON.pretty_generate(current))
previous.keys | current.keys vereinigt die beiden Pfadlisten, sodass wir
jeden Pfad aus beiden Läufen betrachten — so wird ein brandneuer Endpunkt
bemerkt. Dann drei Fälle: ein Pfad nur im neuen Lauf ist NEW, einer nur im alten
ist REMOVED, und einer in beiden, dessen Fingerprint sich änderte, ist
CHANGED. Für Änderungen ist now['keys'] - was['keys'] die
Array-Differenz — die Schlüssel, die auftauchten.
Lauf es einmal, um die Basis zu legen:
$ ruby diff.rb
[*] no snapshot yet — saving baseline
Lauf es erneut, ohne dass sich etwas geändert hat, und es bleibt still — keine Fehlalarme:
$ ruby diff.rb
[*] no changes since last run
Eine Änderung fangen
Simuliere nun, dass die API etwas ausliefert. Füge in labapi.rb einem Markt ein
Feld hinzu — gib ihm ein 'volume' => 1_200 — und starte das Labor neu. Lauf den
Diff erneut:
$ ruby diff.rb
~ CHANGED /api/markets/1 status 200->200 new keys ["volume"]
[!] 1 change(s) — look here
Das Werkzeug bemerkte ein Feld, von dem wir ihm nie erzählten. Bei einem echten
Ziel ist diese Zeile ein Anstoß: ein neues volume an einem Markt ist
wahrscheinlich harmlos, aber ein neues is_admin an einem Benutzer, oder eine
Route, die von 401 auf 200 umsprang, ist genau der Hinweis, bei dem du als
Erster sein willst. Der Bot wacht; du untersuchst nur, was sich bewegte.
Erkenntnisse
- Speichere einen Fingerprint, keine rohen Antworten: Status plus sortierte Schlüssel ist stabil gegen Rauschen und empfindlich für Struktur. Das ist das Signal, das du willst.
each_with_object({})baut eine Sammlung während des Iterierens;File.write/File.readmitJSONsichern sie zwischen Läufen.- Vergleiche mit Set-Logik:
a | b(Vereinigung), um jeden Pfad zu betrachten,a - b(Differenz), um zu finden, was auftauchte.NEW/REMOVED/CHANGEDdeckt es ab. - Der Gewinn ist Triage: neuer Code taucht von selbst auf, sodass deine Aufmerksamkeit zu dem geht, was sich änderte, statt jedes Mal die ganze API neu zu lesen.
Übungen
- Erweitere den Fingerprint, auch die Antwortgröße zu erfassen, und markiere einen Pfad, dessen Körper um mehr als, sagen wir, 20 % wuchs oder schrumpfte — ein billiges Signal, dass sich der Inhalt einer Antwort änderte, selbst wenn ihre Schlüssel es nicht taten.
- Im Moment meldet ein
CHANGEDnur Schlüssel, die hinzugefügt wurden. Melde auch Schlüssel, die entfernt wurden (was['keys'] - now['keys']) — ein verschwindendes Feld kann Auth-Logik brechen und ist einen Blick wert. - Fingerprinte auch die authentifizierte Oberfläche: fingerprinte
/api/meund/api/admin/statsmit einem Token, sodass eine Berechtigungsänderung (ein403, das zu200wird) im Diff auftaucht.
Weiter — Teil 8: das Finale. Wir haben acht Skripte, die jedes denselben
Client neu bauen. Wir falten sie in eine Scanner-Klasse mit einer
Kommandozeilen-Schnittstelle, verdrahten eine Scope-Allowlist, damit sie nur je
das Labor anfassen kann, und lassen sie einen Bericht zur koordinierten
Offenlegung ausgeben — der Sprung von Skripten zu einem Werkzeug, das du
tatsächlich laufen lassen würdest.