// Bug-Bounty-Automatisierung in Ruby · Teil 8
Berichten und den Bot verdrahten
Das Finale. Acht Skripte, die jedes denselben Client neu bauen, werden zu einer Scanner-Klasse mit einer Kommandozeilen-Schnittstelle, einer Scope-Allowlist, die sie physisch außerstande setzt, etwas anderes als das Labor anzufassen, und einem Bericht zur koordinierten Offenlegung als Ausgabe. Klassen, OptionParser und die Ethik, die in den Code selbst gehört.
Du hast acht Bugs gefunden, und jeder Teil baute dieselbe Login-und-Anfrage- Verrohrung neu, um es zu tun. Das ist das Zeichen, dass es Zeit ist, mit dem Skripten aufzuhören und ein Werkzeug zu bauen: ein Objekt, das den Client hält, die Prüfungen ausführt und berichtet, was es fand. Wir verdrahten außerdem das, was ein Bug-Bounty-Werkzeug mehr als jedes Feature braucht — eine Scope-Schranke, die es sich weigern lässt, irgendwo zu laufen, wo es nicht erlaubt ist.
Eine Klasse, die alles hält
Eine Klasse bündelt Daten und die Methoden, die auf ihnen wirken. Unsere hält
eine Basis-URL und eine Liste von Funden. Beginne scanner.rb:
#!/usr/bin/env ruby
require 'net/http'
require 'json'
require 'uri'
require 'optparse'
class Scanner
ALLOWED_HOSTS = %w[127.0.0.1 localhost].freeze
def initialize(base)
@base = base
host = URI(base).host
unless ALLOWED_HOSTS.include?(host)
abort "[!] refusing to scan #{host}: not in the scope allowlist"
end
@findings = []
end
initialize läuft, wenn du Scanner.new(...) aufrufst. Die @base und
@findings sind Instanzvariablen — jeder Scanner trägt seine eigenen. Die
wichtigen Zeilen sind die Schranke: Wir ziehen den Host aus der URL und brechen
mit abort ab, es sei denn, er steht auf ALLOWED_HOSTS. Dieses Werkzeug kann
nicht auf ein echtes Ziel gerichtet werden, weil es aufhört, bevor es ein
einziges Byte sendet. Diese Prüfung ist der wichtigste Code in der Datei.
Jetzt der Client, als Methoden auf der Klasse:
def get(path, token = nil)
uri = URI("#{@base}#{path}")
req = Net::HTTP::Get.new(uri)
req['Authorization'] = "Bearer #{token}" if token
Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) }
end
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 record(title, severity, endpoint, evidence)
@findings << { title: title, severity: severity, endpoint: endpoint, evidence: evidence }
end
Dasselbe get und login wie in jedem Teil zuvor — aber jetzt leben sie
einmal, auf dem Objekt. record hängt einen Fund an @findings an; jeder ist
ein Hash mit allem, was ein Bericht braucht.
Die Prüfungen einfalten
Jeder Bug, den wir von Hand fanden, wird zu einer Prüfmethode. Hier sind zwei aus früheren Teilen, jetzt automatisiert — Objekt- und Funktionsebenen-Autorisierung:
def check_bola(token, me_id)
res = get('/api/users/99', token)
return unless res.code.to_i == 200
victim = JSON.parse(res.body)
return if victim['id'] == me_id
record('Broken object-level authorization (IDOR)', 'high', 'GET /api/users/{id}',
"user #{me_id}'s token read user #{victim['id']} (#{victim['name']})")
end
def check_bfla(token)
res = get('/api/admin/stats', token)
return unless res.code.to_i == 200
record('Broken function-level authorization', 'high', 'GET /api/admin/stats',
"a non-admin token received #{res.body.strip}")
end
Jede Prüfung macht ihren Probe, entscheidet, ob der Fehler vorliegt, und ruft
record, falls ja. return unless ... steigt früh aus, wenn es nichts zu
berichten gibt — eine Prüfung, die nichts findet, fügt einfach nichts hinzu.
Jetzt orchestriert run sie und report gibt das Ergebnis aus:
def run
token = login('alice')
me = JSON.parse(get('/api/me', token).body)
check_bola(token, me['id'])
check_bfla(token)
self
end
def report
puts "# Scan report — #{@base}"
puts "# #{@findings.size} finding(s)"
@findings.each_with_index do |f, i|
puts
puts "## #{i + 1}. #{f[:title]} [#{f[:severity].upcase}]"
puts "Endpoint: #{f[:endpoint]}"
puts "Evidence: #{f[:evidence]}"
end
end
end
run gibt self zurück, sodass wir .run.report verketten können. report
läuft @findings ab und gibt jeden in einer festen Form aus — Titel,
Schweregrad, Endpunkt, Beweis — das Skelett einer Offenlegung.
Eine echte Kommandozeile
Ein Werkzeug nimmt Argumente. OptionParser (Standardbibliothek) verwandelt
--target URL in einen Wert, mit einer Nutzungsmeldung gratis dazu:
options = { target: 'http://127.0.0.1:4567' }
OptionParser.new do |o|
o.banner = 'Usage: ruby scanner.rb [--target URL]'
o.on('--target URL', 'Base URL to scan (must be in scope)') { |v| options[:target] = v }
end.parse!
Scanner.new(options[:target]).run.report
Lauf es ohne Argumente und es scannt das Labor:
$ ruby scanner.rb
# Scan report — http://127.0.0.1:4567
# 2 finding(s)
## 1. Broken object-level authorization (IDOR) [HIGH]
Endpoint: GET /api/users/{id}
Evidence: user 1's token read user 99 (admin)
## 2. Broken function-level authorization [HIGH]
Endpoint: GET /api/admin/stats
Evidence: a non-admin token received {"users":3,"orders":0,"balances":2500}
Dieser Block ist ein Bericht zur koordinierten Offenlegung im Miniaturformat: was, wie schlimm, wo und der Beweis. Es ist das Rohmaterial, das du zu einem Writeup ausbauen und an den Anbieter senden würdest — und, sobald er es behoben hat, veröffentlichst.
Die Schranke verdient ihr Geld
Richte dasselbe Werkzeug auf etwas, das dir nicht gehört, und es stoppt sich selbst:
$ ruby scanner.rb --target https://api.example.com
[!] refusing to scan api.example.com: not in the scope allowlist
Keine Anfrage geht raus. Das ist der Unterschied zwischen einem Werkzeug und einer Haftung: Die Scope-Prüfung ist kein Kommentar, der dich an Vorsicht erinnert, sie ist Code, der Unvorsichtigkeit unmöglich macht. Wenn du dieses Muster zu einem echten Programm mitnimmst, wird die Allowlist zu genau den Hosts deiner Autorisierung — und zu nichts sonst.
Erkenntnisse
- Eine Klasse bündelt Zustand (
@base,@findings) mit den Methoden, die ihn nutzen;initializerichtet ihn ein, Instanzmethoden wirken darauf,selflässt dich verketten. - Falte wiederholte Verrohrung einmal ins Objekt; jeder von Hand gefundene
Bug wird zu einer
check_*-Methode, die probt undrecordruft. OptionParsergibt dir eine echte CLI — Flags, Standardwerte und ein Nutzungs-Banner — aus ein paar Zeilen Standardbibliothek.- Bring die Ethik in den Code: eine Scope-Allowlist, die bei jedem Host, den
zu testen du nicht autorisiert warst, mit
abortabbricht, ist die einzige wichtigste Zeile in einem Scanner. Ein Bericht ist das Ergebnis; der Scope ist, was dich auf der richtigen Seite davon hält.
Übungen
- Füge
check_mass_assignmentundcheck_raceaus den Teilen 5 und 6 als Methoden hinzu, sodass einrunalle vier Bug-Klassen abdeckt und sie zusammen berichtet. - Füge eine
--report json-Option hinzu, die@findingsals JSON statt als Text ausgibt, sodass die Ausgabe ein Ticketsystem oder den Diff aus Teil 7 speisen kann. - Lade die Allowlist aus einem
--scope host1,host2-Flag (weiterhin mit localhost als Standard). So würdest du das Werkzeug auf die autorisierten Hosts eines echten Programms richten, ohne sie je fest zu verdrahten.
Das ist die Reihe: von einer ersten HTTP-Anfrage zu einem gescopeten, selbstberichtenden Scanner, der acht Klassen von API-Bugs automatisiert. Die Techniken sind dieselben, mit denen echte Bug-Bounty-Automatisierung läuft — das Einzige, was sich bei einem Live-Programm ändert, ist, dass die Scope-Allowlist Hosts enthält, die zu testen dir erlaubt wurde, und dass der Bericht in einer Offenlegung endet statt in einem Labor.