// 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.

RubyDiffingFilesSets
5 Min. Lesezeit

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.read mit JSON sichern 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 / CHANGED deckt 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

  1. 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.
  2. Im Moment meldet ein CHANGED nur 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.
  3. Fingerprinte auch die authentifizierte Oberfläche: fingerprinte /api/me und /api/admin/stats mit einem Token, sodass eine Berechtigungsänderung (ein 403, das zu 200 wird) 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.

← Alle Teile