본문 바로가기
CVE 취약점

Keycloak reset-credentials 플로우 인증 우회로 인한 계정 탈취 분석 (CVE-2026-18963)

by WhiteGuidance 2026. 8. 30.
반응형

 

무슨 일이 가능했나

인증되지 않은 원격 공격자가, 피해자의 아이디(또는 이메일)만 알고 있으면 이메일로 온 재설정 링크를 단 한 번도 클릭하지 않고도 그 계정의 비밀번호를 임의로 설정할 수 있었다. 사용자 상호작용도, 사전 자격증명도 필요 없다. CVSS 3.1 기준 9.1점(Critical)이 매겨진 이유다.

항목 내용
CVE ID CVE-2026-18963
컴포넌트 keycloak-services (reset-credentials 인증 플로우)
CVSS 3.1 9.1 (Critical) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
CWE CWE-640 (Weak Password Recovery Mechanism for Forgotten Password)
패치 버전 26.7.2 / 26.6.6 / 26.4.15(LTS) / 26.8.0

아래에서는 "공격자가 실제로 무엇을 순서대로 밟았을지"를 기준으로, 각 단계마다 그 단계를 가능하게 만든 실제 소스코드를 그 자리에서 함께 짚어본다.


알아야 할 최소한의 배경 지식

Keycloak의 로그인/비밀번호 재설정 절차는 여러 Authenticator를 순서대로 실행하는 "플로우(Flow)"다. 각 Authenticator는 화면을 보여줄 때 호출되는 authenticate(context)와, 사용자가 폼을 제출했을 때 호출되는 action(context) 두 메서드를 가진다. reset-credentials 플로우는 reset-credentials-choose-user(사용자 지정) → reset-credential-email(이메일 발송 및 대기) 순서로 구성된다.

 

이메일 링크를 클릭하면 별도의 Action Token 검증 경로가 같은 플로우를 다시 실행시키면서, 인증 세션에 ACTION_TOKEN_USER_ID라는 note를 남긴다. 이 값이 현재 세션의 대상 사용자와 일치하는지가, "이메일 인증을 실제로 완료했는가"를 판단하는 유일한 근거다. 이 글의 핵심은 결국 이 값을 확인하는 지점이 코드 한 곳에서 누락되어 있었다는 것이다.


1단계 — 피해자 계정 지정

공격자는 reset-credentials-choose-user 단계에 피해자의 아이디를 제출한다. 이 단계는 실제로 아래 코드만 수행한다.

 

context.setUser(user)가 호출되는 시점에는 아직 어떤 이메일도 발송되지 않았고, 어떤 인증도 이뤄지지 않았다. 그냥 "이 세션의 대상은 이 사용자다"라는 포인터 하나가 세워질 뿐이다. 이 포인터가 뒤 단계에서 그대로 신뢰된다는 점이 이 취약점 전체의 전제 조건이다.


2단계 — 이메일 발송·대기 단계로 진입

플로우는 reset-credential-email로 넘어간다. 정상적인 사용자라면 여기서 다음 코드를 거쳐 메일을 받고 대기 화면만 보게 된다.

 

이 시점에서 ACTION_TOKEN_USER_ID는 세션에 존재하지 않는다. 공격자는 이메일을 열어볼 수 없으므로 이 경로로는 더 나아갈 수 없다 — authenticate()만 놓고 보면 여기서 막힌다. 공격자가 노린 것은 이 화면으로 이어지는 action() 쪽이었다.


3단계 — 화면 상태를 조작해 검증 없는 경로로 우회 진입

Keycloak은 URL의 execution 파라미터로 "이번 요청이 어느 Authenticator를 대상으로 한 것인지" 판단한다. 여기서 두 번째 결함이 등장한다. DefaultAuthenticationFlow.java의 "다른 방법으로" 화면 전환 로직이다.

 

AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED는 세션 전역에 걸린 "true"/"false" 값 하나일 뿐, 어떤 execution에서 이 화면이 떴는지에 대한 정보를 담고 있지 않다. 반면 CURRENT_AUTHENTICATION_EXECUTION은 플로우가 진행되며 계속 갱신되는 값이다. 이 둘을 조합해 화면을 재구성하는 로직은, "예전에 어디선가 셀렉터 화면이 뜬 적이 있다"는 사실과 "지금 마지막으로 다뤄진 execution이 무엇인가"라는 사실을 잘못 엮어버린다 — 이 둘은 원래 같은 execution을 가리켜야 한다는 보장이 없었다.

 

이 상태 조작을 통해, 공격자는 reset-credential-email execution을 대상으로 한 요청이 authenticate()를 거치지 않고 곧바로 action()으로 라우팅되도록 세션 상태를 유도할 수 있었다.


검증 없이 통과되는 지점: ResetCredentialEmail.action()

이 취약점의 실질적인 몸통이다. 패치 전 코드는 다음이 전부였다.

 

 

한 줄이다. action()이 어떤 경위로 호출되든 — ACTION_TOKEN_USER_ID가 세션에 있는지, 있다면 지금의 대상 사용자와 일치하는지 — 아무것도 확인하지 않고 무조건 성공 처리한다. authenticate()에서 하던 검증(Objects.equals(user.getId(), actionTokenUserId))이 action()에는 존재하지 않았다.

 

3단계에서 만든 우회 경로와 이 지점이 만나는 순간, 이메일 인증 단계 전체가 사실상 통과 처리된다.


5단계 — 비밀번호 재설정, 계정 탈취

플로우는 다음 단계인 비밀번호 설정 화면으로 넘어간다. 1단계에서 context.setUser(victim)으로 고정된 사용자가 그대로 대상이므로, 공격자는 피해자 계정의 새 비밀번호를 직접 입력해 세션을 완료할 수 있다. 이메일 링크를 클릭한 적이 없는데도 계정을 완전히 장악하게 되는 지점이다.


패치는 무엇을 바꿨나

① ResetCredentialEmail.java — 4단계에서 빠졌던 검증을 그대로 추가

 

 

authenticate()가 이미 하고 있던 검증을 action()에도 강제한다. 이제는 실제로 이메일 링크를 클릭해 발급된 ACTION_TOKEN_USER_ID가 현재 세션의 사용자와 정확히 일치할 때만 통과한다.

 

AuthenticationProcessor.java / DefaultAuthenticationFlow.java — 3단계의 상태-실행 단계 바인딩 결함 수정

 

단순 "true" 대신 셀렉터 화면을 띄운 execution의 모델 ID 자체를 저장하고, 재진입 시 문자열이 정확히 일치할 때만 화면을 재구성하도록 바꿨다. 화면 상태와 실행 단계가 1:1로 바인딩된 것이다. 두 결함이 각각 독립적으로 막히면서 5단계로 이어지는 경로 전체가 차단된다.


실습 영상

 


영향 범위 및 조치

스트림 취약 버전 패치 버전
Keycloak 26.7.x 26.7.2 미만 26.7.2
Keycloak 26.6.x 26.6.6 미만 26.6.6
Keycloak 26.4.x (LTS) 26.4.15 미만 26.4.15
Keycloak 26.8.x 26.8.0 미만 26.8.0

 

지원 종료된 26.5.x 이하 버전은 별도 패치가 없으므로 지원 스트림으로의 업그레이드가 유일한 해결책이다. 즉시 패치가 어렵다면 realm 설정에서 resetPasswordAllowed를 비활성화해 진입점 자체(2단계)를 차단할 수 있지만, 이는 정상 사용자의 셀프서비스 비밀번호 재설정도 함께 막는 임시 조치일 뿐이다.

반응형