
Эксплойт для Keycloak CVE-2026-18963, позволяющий неавторизованный захват учётной записи через обход сброса учётных данных. Включает безопасное обнаружение, неразрушающее доказательство, полный захват, перечисление имён пользователей и лабораторию с уязвимой и исправленной версиями.
Зная только имя пользователя или адрес электронной почты, неаутентифицированный злоумышленник устанавливает произвольный пароль на любой учётной записи Keycloak. Письмо о сбросе пароля доставляется реальной жертве и никогда не требуется — злоумышленник не читает почтовый ящик, не переходит по ссылкам и не имеет ни предыдущих учётных данных, ни сессии.
Затронуто: Keycloak 26.0.0 – 26.7.1. Исправлено в 26.7.2.
Одна команда. Действительное имя пользователя не требуется, и побочных эффектов нет — она не отправляет письмо, не изменяет ни одну учётную запись и останавливается до эксплуатируемого шага.```bash git clone https://github.com/Snizi/CVE-2026-18963-Exploit cd CVE-2026-18963-Exploit
python3 cve_2026_18963_poc.py
--base https://sso.example.com --realm YOUR_REALM
--client-id account
--redirect-uri https://sso.example.com/realms/YOUR_REALM/account/
--safe-check
Python 3.9+, только стандартная библиотека. Ничего устанавливать не нужно.
| Код выхода | Вердикт | Значение |
|:---:|---|---|
| `0` | 🔴 **УЯЗВИМО** | был отдан припаркованный почтовый шлюз — сама ошибка |
| `2` | 🟢 **ИСПРАВЛЕНО** | поток переключился на вход и остался там (исправление #51844 присутствует) |
| `2` | 🟡 **СМЯГЧЕНО** | сброс учётных данных недоступен — *Забыли пароль* отключён. **Это не исправление.** |
| `3` | ⚪ **НЕОПРЕДЕЛЁННО** | нераспознанный ответ — **не воспринимайте это как прохождение проверки** |
Запускайте для каждого realm — *Забыли пароль* — это настройка на уровне realm, и `master` тоже учитывается.
Подробности и объяснение, почему проверке не нужен пользователь и она ничего не затрагивает, — в
[§4a](#4a-safe-detection---safe-check--start-here).
**Уже знаете, что вы уязвимы?** Переходите к [устранению](#8-remediation) и
[обнаружению / охоте за угрозами](#9-detection).
### Попробуйте без цели
В репозитории есть лаборатория, которая запускает уязвимую **26.7.1** и исправленную **26.7.2**
бок о бок с одинаковым realm, а также почтовый ящик, чтобы увидеть, как письмо о сбросе
приходит и остаётся непрочитанным, пока учётная запись захватывается:```bash
cd lab && docker compose up -d
python3 ../cve_2026_18963_poc.py --base http://localhost:8080 \
--realm poc --client-id poc-app --safe-check # VULNERABLE
python3 ../cve_2026_18963_poc.py --base http://localhost:8100 \
--realm poc --client-id poc-app --safe-check # PATCHED
⚠️ Только авторизованное тестирование
Этот репозиторий предназначен для защитников, специалистов по реагированию на инциденты и авторизованных пентестеров. Запускайте его только против систем, которыми вы владеете или на тестирование которых у вас есть письменное разрешение. Всё здесь поставляется с самодостаточной уязвимой лабораторией (
lab/), поэтому для изучения работы бага не требуется трогать ничего внешнего. Направление его на стороннюю инфраструктуру без авторизации незаконно в большинстве юрисдикций и не поддерживается этим проектом.
Ссылки: keycloak#51833 · GHSA-4gv3-mc9p-5wqc · исправление keycloak#51844
Два дефекта, объединённые в цепочку. Ни один из них не эксплуатируется по отдельности.
services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
processAction() — любой POST, содержащий ключ формы tryAnotherWay:```java
processor.getAuthenticationSession().setAuthNote(
AuthenticationProcessor.AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED, "true");
return createSelectAuthenticatorsScreen(model);
Заметка представляет собой **голый булев флаг без записи о том, какой набор выполнения его установил**. Он сбрасывается только в ветке, которая обрабатывает переданный параметр `authenticationExecution`. Если опустить этот параметр — как это делает данный PoC на протяжении всего кода — флаг остаётся установленным на всё время жизни сессии аутентификации.
`processFlow()` — пока флаг истинен, обычная оценка потока пропускается:```java
if (Boolean.parseBoolean(authSession.getAuthNote(AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED))) {
String lastExecutionId = authSession.getAuthNote(CURRENT_AUTHENTICATION_EXECUTION);
if (lastExecutionId != null) {
AuthenticationExecutionModel executionModel =
realm.getAuthenticationExecutionById(lastExecutionId);
if (executionModel != null)
return createSelectAuthenticatorsScreen(executionModel); // <-- attacker-usable form
}
}
It renders a submittable form aimed at whatever execution is currently parked, instead of keeping the session pinned on "waiting for the e-mail".
The glue is processResult() case FORK: — when Send Reset Email fires it stamps
CURRENT_AUTHENTICATION_EXECUTION = <reset-credential-email execution id> and forks
the browser to the login page. The parked execution is precisely the e-mail gate.
`services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java````java @Override public void action(AuthenticationFlowContext context) { context.getUser().setEmailVerified(true); context.success(); }
Unconditional. Ничто не проверяет, что поток был возобновлён действительным токеном действия,
поэтому *достижение* `action()` рассматривается как эквивалент доказательства контроля над почтовым ящиком.
### Цепочка```
tryAnotherWay POST → sticky AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED="true"
submit victim identifier → mail sent to victim, e-mail execution parked (FORK)
re-enter reset-credentials → sticky flag serves a form targeting the parked e-mail execution
POST that form → ResetCredentialEmail.action() → success() → gate bypassed
→ flow advances to UPDATE_PASSWORD → attacker sets the password
Шесть HTTP-запросов, без аутентификации, параметр authenticationExecution не используется ни в один момент.