Race Condition (TOCTOU) - Timing-basierte Sicherheitsschwachstelle
Race Conditions (CWE-362) entstehen wenn die Sicherheit eines Systems davon abhängt dass zwei oder mehr Operationen in einer bestimmten Reihenfolge ausgeführt werden, aber parallele Ausführung diese Reihenfolge verletzt. TOCTOU (Time-Of-Check Time-Of-Use) ist die häufigste Form: Prüfung und Nutzung einer Ressource sind zeitlich getrennt. Sicherheitsrelevante Auswirkungen: Doppel-Ausgaben in Finanzanwendungen (Double-Spending), Privilege-Escalation über temporäre Dateien, Discount-Missbrauch, Account-Übernahme. Schutz: atomare Datenbankoperationen, Mutexe, optimistic locking.
Race Conditions gehören zu den schwieriger zu findenden aber oft hochimpactigen Schwachstellen. Ein klassisches Beispiel: Online-Banking-Anwendung prüft Kontostand (€50) und überweist dann. Was wenn ein Angreifer 100 Überweisungsanfragen gleichzeitig sendet? Jede Prüfung sieht €50, jede Überweisung wird ausgeführt, bevor das Konto aktualisiert wird. Ergebnis: €5.000 abgebucht statt €50.
TOCTOU - Time Of Check / Time Of Use
Grundprinzip
Normaler (unsicherer) Ablauf:
t=0: PRÜFUNG: Kontostand = €50, Überweisung €50 → OKt=1: AKTION: Buche €50 ab, Kontostand = €0
Race Condition (parallele Anfragen):
- Anfrage A,
t=0: PRÜFUNG: Kontostand = €50 → OK - Anfrage B,
t=1: PRÜFUNG: Kontostand = €50 → OK (noch nicht aktualisiert!) - Anfrage A,
t=2: AKTION: Buche €50 ab, Kontostand = €0 - Anfrage B,
t=3: AKTION: Buche €50 ab, Kontostand = -€50 (!)
Das Zeitfenster zwischen CHECK und USE = “Race Window”. Je größer das Race Window, desto leichter ausnutzbar. Datenbank-Calls, Netzwerk-Requests und I/O vergrößern das Fenster.
TOCTOU-Kategorien
1. Datei-System-TOCTOU:
// Prüfen ob Datei existiert:
if (!file_exists($filename)) {
file_put_contents($filename, $data); // ← Zwischen check + use: Symlink!
}
// Angreifer erstellt Symlink zwischen file_exists() und file_put_contents()
// Schreibt Daten in /etc/passwd o.ä. (wenn Prozess Root-Rechte hat)
2. Web-Anwendungs-TOCTOU (Business Logic):
- Prüfen ob Benutzer genug Guthaben hat
- Guthaben abbuchen
- Race Window: parallele HTTP-Requests senden
3. Betriebssystem-Level:
- Prozesse mit SUID-Bit: Datei-Existenz prüfen, dann öffnen
- Zwischen Prüfung und Öffnen: Datei austauschen
- Privilegierter Prozess öffnet nun andere Datei als erwartet
Auswirkungen in Web-Anwendungen
1. Gift-Card / Coupon-Codes mehrfach einlösen
Normaler Ablauf:
- Prüfe: ist Gutschein gültig? (Status = unused)
- Wende Rabatt an
- Markiere Gutschein als used
Race Condition:
- Angreifer sendet 50 parallele Requests mit demselben Code
- Alle prüfen: Status = unused → alle OK
- Alle wenden Rabatt an (bevor einer “used” setzt)
- Gutschein 50x eingelöst!
Nachweis: Burp Suite Turbo Intruder
POST /apply-coupon { "code": "SAVE50" }
→ 50 parallele Requests → prüfen ob mehrfach gutgeschrieben
2. Doppel-Ausgaben / Double-Spending
Krypto-Wallet oder E-Commerce:
- Prüfe: Wallet-Guthaben ≥ 100
- Sende Transaktion: -100
- Aktualisiere Wallet: -100
Race: parallele Transaktionen senden, beide prüfen 100 ≥ 100 → true. Beide Transaktionen werden ausgeführt → effektiv wird 200 aus einem 100-Guthaben verbraucht.
3. Limit-Umgehung (Rate Limiting via DB)
“User darf max. 1 Konto erstellen”:
- Prüfe: hat User schon Account? (COUNT(*) = 0)
- Erstelle Account
- Ergebnis: Account existiert
Race: 100 parallele Registrierungsanfragen → alle prüfen: 0 Accounts → true → alle erstellen Account → User hat 100 Accounts!
4. TOCTOU bei Privilegien-Änderung
Admin-Panel: “Deaktiviere User-Account”:
- Prüfe: ist Account aktiv? (active = true)
- Invalide Sessions
- Setze active = false
Race: User sendet Request im selben Moment → Check: active = true → OK → Deaktivierung läuft → User-Request passiert zwischen Prüfung und Deaktivierung → noch autorisiert!
Erkennung und Testing
Burp Suite Turbo Intruder
Speziell für Race-Condition-Tests optimiert: HTTP-Pipelining + simultane TCP-Verbindungen, sehr präzises Timing (ms-Genauigkeit).
Basic Race Condition Test:
- HTTP-Request identifizieren (z.B. POST /apply-coupon)
- In Burp: Send to Turbo Intruder
- Payload: 50 Requests mit race=true
- Send simultaneously (nicht sequentiell!)
- Responses analysieren: mehr als 1 Erfolg?
Turbo Intruder Konfiguration:
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=50,
pipeline=False) # Separate Verbindungen!
for i in range(50):
engine.queue(target.req) # Alle sofort einreihen
def handleResponse(req, interesting):
if '200' in req.status:
table.add(req) # Erfolgreiche Responses markieren
HTTP/2 Single-Packet Attack:
- Alle Requests in ein einziges TCP-Paket
- Maximale Synchronisation → höchste Race-Window-Trefferquote
- Unterstützt ab Burp Suite 2022.9
Testing-Schritte für Business Logic
- Gift-Cards: denselben Code 20x gleichzeitig einlösen
- Überweisungen: denselben Betrag 10x gleichzeitig transferieren
- Registrierung: denselben Username 10x gleichzeitig anlegen
- Passwort-Reset: denselben Token 5x gleichzeitig einlösen
- Discount: denselben Code 20x gleichzeitig anwenden
Indikatoren für Race Conditions
- Multiple “success” Responses auf identische parallele Anfragen
- Unterschiedliche Response-Zeiten bei Parallel-Tests
- Inkonsistente Datenbankzustände nach Tests
Schutzmaßnahmen
1. Datenbankebene - Atomare Operationen
-- Unsicher:
SELECT balance FROM accounts WHERE id = 123; -- balance = 100
UPDATE accounts SET balance = balance - 50 WHERE id = 123;
-- Sicher (atomare Operation):
UPDATE accounts
SET balance = balance - 50
WHERE id = 123 AND balance >= 50;
-- Affected rows: 0 → Retry oder Fehler; Affected rows: 1 → Erfolg!
-- SELECT FOR UPDATE (Pessimistic Locking):
BEGIN TRANSACTION;
SELECT balance FROM accounts WHERE id = 123 FOR UPDATE;
-- Zeile ist jetzt gelockt! Kein anderer Thread kann sie ändern
UPDATE accounts SET balance = balance - 50 WHERE id = 123;
COMMIT;
2. Datenbankebene - Unique Constraints
-- Verhindert Doppel-Ausgabe von Gutscheincodes:
CREATE TABLE coupon_redemptions (
coupon_code VARCHAR(50) NOT NULL,
user_id INT NOT NULL,
UNIQUE (coupon_code, user_id) -- DB verhindert Duplikat!
);
-- INSERT wird fehlschlagen wenn bereits vorhanden → Race verhindert!
3. Anwendungsebene - Mutex/Semaphore
import threading
lock = threading.Lock()
def transfer_money(from_account, to_account, amount):
with lock: # ← Exklusiver Zugriff
balance = get_balance(from_account)
if balance >= amount:
set_balance(from_account, balance - amount)
set_balance(to_account, get_balance(to_account) + amount)
# Redis-basiertes Distributed Locking (für skalierte Apps):
import redis
r = redis.Redis()
lock_key = f"transfer_lock_{account_id}"
if r.set(lock_key, "1", nx=True, ex=5): # NX = nur wenn nicht existiert
try:
perform_transfer()
finally:
r.delete(lock_key)
else:
raise Exception("Transfer already in progress, retry")
4. Idempotency Keys (für APIs)
Jede kritische Aktion bekommt einen einzigartigen Key:
POST /api/transfer
Idempotency-Key: uuid-generated-by-client
Server-side:
- Prüfe: wurde dieser Key bereits verarbeitet?
- Ja: gebe gecachte Antwort zurück (kein erneutes Ausführen!)
- Nein: verarbeite und speichere Key mit Ergebnis
Verhindert Doppel-Ausführung selbst bei Netzwerk-Retries. Standard in Payment-APIs (Stripe, PayPal nutzen dieses Muster).
5. TOCTOU im Dateisystem
// Verwende O_EXCL + O_CREAT (atomares Erstellen):
fd = open(filename, O_WRONLY | O_CREAT | O_EXCL, 0600);
if (fd == -1 && errno == EEXIST) {
// Datei existiert bereits → Fehler
}
// O_EXCL: Fehler wenn Datei schon existiert
// Atomisch: kein Race zwischen Prüfung und Erstellung!