
Доказательство концепции и средство проверки уязвимости для CVE-2026-18963 — неаутентифицированного захвата учётной записи в потоке сброса учётных данных Keycloak. Однофайловый Python, опциональный браузерный компонент Playwright.
Доказательство концепции и средство проверки уязвимости для обхода потока сброса учётных данных в Keycloak, с опциональным браузерным компонентом, который отображает результирующее состояние в реальном окне Chrome.
Инструмент представляет собой один файл, cve_2026_18963_poc.py, написанный на стандартной библиотеке Python. Playwright — единственная сторонняя зависимость, и он импортируется лениво, поэтому HTTP-часть работает на стандартном интерпретаторе без установки чего-либо.
cve_2026_18963_poc.py 1009 lines, Python 3.10 or newer
Благодарность. CVE-2026-18963 была сообщена Red Hat Джеймсом Паремайном, который упомянут в рекомендациях вендора. Orevim не обнаружил и не сообщил об этой уязвимости. Этот репозиторий представляет собой независимое доказательство концепции, написанное на основе опубликованных рекомендаций.
CVE-2026-18963 — это обход аутентификации в процессе восстановления пароля Keycloak. Это два дефекта, действующих совместно.
Заметка без области действия. Когда пользователь открывает экран выбора аутентификатора,
Keycloak записывает заметку AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED в
сессию аутентификации. Заметка представляет собой простой логический флаг. Она фиксирует, что
какой-то экран выбора был отображён, а не какое выполнение его отобразило. Заметка
переживает выполнение, которое её установило.
Незащищённый обработчик. ResetCredentialEmail.action() устанавливает emailVerified
для разрешённого пользователя и вызывает success() без проверки токена действия,
который должен нести отправленный по электронной почте ссылке. Метод предполагает, что до него
можно добраться только по этой ссылке.
В цепочке неаутентифицированный вызывающий устанавливает заметку для выполнения
ввода имени пользователя, продвигает поток так, чтобы выполнение электронной почты стало текущим, а затем
повторно входит по URL сброса. Устаревшая заметка заставляет экран выбора отображаться
привязанным к выполнению электронной почты. Отправка пустого тела в loginAction этого выполнения
напрямую вызывает незащищённый обработчик. Поток переходит к
обязательному действию UPDATE_PASSWORD без какого-либо чтения электронной почты.
| Идентификатор | CVE-2026-18963, GHSA-4gv3-mc9p-5wqc |
| Класс | CWE-640, слабый механизм восстановления пароля для забытого пароля |
| CVSS v3.1 | 9.1 AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Затронуто | >= 26.0.0, < 26.4.15, >= 26.5.0, < 26.6.6, >= 26.7.0, < 26.7.2 |
| Исправлено в | 26.4.15, 26.6.6, 26.7.2. Ветка 26.8.0 поставляется с исправлением |
| Пакет | org.keycloak:keycloak-services (Maven) |
| Опубликовано | 18 августа 2026 г. |
| Сообщено | Джеймсом Паремайном, упомянут в рекомендациях Red Hat |
Единственное предварительное условие — realm предлагает восстановление пароля неаутентифицированным пользователям. Ни учётные данные, ни токен, ни предыдущая сессия не требуются.
Восемь запросов по порядку. Теги соответствуют меткам шагов, которые выводит инструмент, и
именам файлов, которые он записывает в --out.
| # | Тег | Метод | Что делает |
|---|---|---|---|
| 1 | login | GET | URL авторизации. Устанавливает сессию аутентификации и выдаёт ссылку сброса. |
| 2 | reset | GET | Страница сброса учётных данных. Содержит форму имени пользователя. |
| 3 | tryanother | POST | tryAnotherWay=on. Записывает заметку без области действия. |
| 4 | username | POST | Идентификатор. Продвигает поток так, чтобы выполнение электронной почты стало текущим. Отправляет письмо для восстановления, если идентификатор разрешается. |
| 5 | reenter | GET | Снова URL сброса. Устаревшая заметка отображает селектор, привязанный к выполнению электронной почты. |
| 6 | action | POST | Пустое тело в loginAction этого выполнения. Вызывает незащищённый обработчик. |
| 7 | required-action | GET | Переходит по Location на страницу UPDATE_PASSWORD. |
| 8 | setpassword | POST | Новый пароль. Отправляется только с --set-password. |
Шаг 6 — это эксплойт, и пустое тело имеет решающее значение. Отправка
authenticationExecution=<id> вместо этого является обычным взаимодействием с селектором: оно
переключает выполнение и повторно отправляет письмо для восстановления, что никогда не достигает
дефекта. Пустой POST не отвечает ни на какой вызов, поэтому Keycloak вызывает обработчик для
того выполнения, которое устаревшая заметка оставила текущим.
# Decide whether the instance is affected. Changes nothing, sends no mail.
./cve_2026_18963_poc.py --url https://sso.example.com --realm demo \
--client-id account-console --check
# Additionally report which identifiers resolve to a real account.
# Resolving accounts receive a recovery email. See the note below.
./cve_2026_18963_poc.py --url https://sso.example.com --realm demo \
--client-id account-console --check --users-file names.txt --rate 0.25
# Reach the set-password form for one account and stop there.
./cve_2026_18963_poc.py --url https://sso.example.com --realm demo \
--client-id account-console --username [email protected]
# Complete the takeover. This locks the account holder out.
./cve_2026_18963_poc.py --url https://sso.example.com --realm demo \
--client-id account-console --username [email protected] \
--set-password 'N3w-passw0rd!'
--check выполняет проверку со случайным идентификатором вида
<16 hex>@<8 hex>.invalid. Домен .invalid зарезервирован RFC 2606 и
не может быть разрешён, поэтому проверка не может совпасть с реальной учётной записью и не может
привести к отправке почты. Она устанавливает статус сборки, никого не затрагивая.
Добавление
--usersменяет это. Шаг 4 — это обычная отправка формы восстановления пароля. В realm с настроенным SMTP идентификатор, который разрешается в пользователя с адресом электронной почты, получает настоящее письмо для восстановления пароля — как на уязвимых, так и на исправленных сборках. Простая проверка этого не делает, потому чтоResetCredentialEmail.authenticate()ничего не отправляет, когда пользователь равен null. Рассматривайте--check --usersкак процесс, который уведомляет реальных людей, и подтвердите, что он входит в ваши полномочия, прежде чем запускать его.
Инструмент классифицирует по ответу на шаг 6 — пустой POST.
| Ответ | Результат | Значение |
|---|---|---|
| 400 | account-absent | Обработчик выполнился без токена действия и не разрешил ни одного пользователя. Сборка уязвима. |
302 с Location | form-reached | Обработчик выполнился, и учётная запись разрешается. Уязвима, и этот идентификатор реальный. |
| Что-либо иное | not-vulnerable | Пустой POST не продвинулся. |
| Нет селектора на шаге 5 | not-vulnerable | Устаревшая заметка не была учтена. |
| 302 после шага 8 | password-set | Пароль изменён, и realm выдал сессию. |
| 429 после исчерпания бюджета повторов | rate-limited | Вердикта нет. Запуск остановлен. |
| Отрицательный результат, полученный после любого троттлинга | inconclusive | См. ниже. |
400 — самый важный. Он отделяет сборка уязвима от эта
учётная запись существует, что позволяет --check установить наличие уязвимости на экземпляре,
не перечисляя ни одного реального пользователя. Разрешение идентификаторов — тогда
отдельный вопрос, требующий явного согласия.