
Keycloak 데이터베이스에서 CVE-2026-18963 악용 흔적(Keycloak 인증되지 않은 계정 탈취)을 탐색합니다.
Keycloak PostgreSQL 데이터베이스에서 CVE-2026-18963 익스플로잇(자격 증명 재설정
흐름을 통한 비인증 계정 탈취)의 흔적을 검색하는 psql 스크립트입니다.
KYOS에서 게시했습니다. 당사가 운영하는 Keycloak 배포 환경을 패치하는 과정에서, 노출 기간 동안 누구도 계정을 탈취당하지 않았는지 확인하기 위해 작성했습니다.
CVE-2026-18963은 keycloak-services의 자격 증명 재설정 흐름에 존재하는 결함입니다
(keycloak#51833). 비인증
공격자는 메일 인증 링크를 클릭하지 않고도 임의 사용자의 비밀번호 재설정 절차를
완료한 뒤, 해당 계정에 새 비밀번호를 설정할 수 있습니다. 사용자 상호작용은
필요하지 않습니다.
일회용 인스턴스의 Keycloak 26.7.1에서 검증했습니다. 재현 절차는 게시하지 않습니다. 아래 세부 내용은 쿼리를 읽는 데 필요한 것들입니다.
이 흐름은 현재 어떤 인증기 화면에 있는지를, 그것을 설정한 단계와 연결되지 않은
방식으로 추적합니다. 이로 인해 자격 증명 재설정 흐름이 메일 인증 단계에서 다시
진입될 수 있으며, 여기서 취약한 ResetCredentialEmail.action()이 액션 토큰을
전혀 확인하지 않은 채 성공을 반환합니다. 이후 실행은 비밀번호 업데이트 폼으로
계속 진행됩니다.
두 가지 결과가 이 저장소의 모든 쿼리를 규정합니다:
SEND_RESET_PASSWORD 이벤트를 기록합니다.
해당 이벤트의 부재는 시그니처가 아닙니다.action()은 반환하기 전에 계정의 이메일 주소를 인증됨으로
표시합니다. 정상 경로는 action()에 전혀 도달하지 않으므로, 이는 우회만이
만들어내는 상태 변경입니다. Q3가 이에 근거합니다.수정 사항(keycloak#51844)은
화면 상태를 그것을 설정한 실행과 연결하고, 해당 사용자에 대한 액션 토큰이 존재할
때만 action()이 성공하도록 합니다.
심각도: Critical, CVSS v3.1 9.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N),
Red Hat 및
NVD 기준. 근본 원인은 인증
흐름의 부적절한 상태 검증이며,
keycloak#51844에서
수정되었습니다.
| 스트림 | 수정 버전 |
|---|---|
| 26.7.x | 26.7.2 (릴리스 노트) |
| 26.6.x | 26.6.6 |
| 26.4.x (LTS) | 26.4.15 |
| 26.8 | 26.8.0 |
지원되는 스트림에서 해당 버전 미만은 모두 취약합니다. 지원이 종료된 릴리스 (26.5.x, 26.3 및 그 이전)는 수정 사항이 제공되지 않습니다. 노출되었다고 가정하고 수정된 스트림으로 업그레이드하십시오. Red Hat은 레거시 RH-SSO 7을 영향받지 않는 것으로 표시합니다. Red Hat Build of Keycloak 26.4/26.6은 26.4.15-1 / 26.6.6-1에서 수정되었습니다.
셀프 서비스 비밀번호 재설정을 비활성화하면 취약한 진입점이 제거됩니다:
관리 콘솔 > Realm settings > Login > 모든 realm에 대해 "Forgot password" 끄기.
관리 API를 통해서는 PUT /admin/realms/{realm}에
{"resetPasswordAllowed": false}를 사용합니다.
이는 login-actions/reset-credentials 엔드포인트를 차단하지만, 정당한 사용자가
자신의 비밀번호를 재설정하는 것도 막으므로 업그레이드 전까지의 임시 조치로
취급하십시오. Red Hat은 이 CVE에 대해 공인된 완화 조치를 제시하지 않습니다.
패치가 유일한 실질적 해결책입니다.
이 스크립트의 첫 번째 버전은 완료된 재설정 직전에 SEND_RESET_PASSWORD가
없는 경우를 플래그로 표시했습니다.
그것은 잘못된 판단이었고,
vilmosnagy가 이를 지적했다.
그는 자신의 인스턴스에서 CVE를 재현하고서도 이벤트가 그대로 나타나는 것을 확인한 뒤
이를 지적했다. 그 이유는 위의 첫 번째 결과에 있다. 메일 인증기는 정상적으로
실행되므로, 피해자는 실제 비밀번호 재설정 메일을 받고 Keycloak은 평범한
SEND_RESET_PASSWORD를 기록한다. 검사가 반대로 되어 있었다. 그 검사는 살펴본
모든 침해된 계정을 지워버렸을 것이다. 그래서 제거되었다.
타이밍 기법에는 더 작지만 실질적인 문제가 있다.
thomasdelorge의 헌트 스크립트에
담긴 접근 방식은 공유된 code_id를 기준으로 SEND_RESET_PASSWORD와 UPDATE_PASSWORD를
연관 짓고 짧은 간격을 표시한다. 연관 키는 맞고, 작성자도 임계값이 휴리스틱임을
분명히 밝히고 있다. 우리의 테스트도 같은 결론이다. 우회된 재설정과 같은 브라우저에서
완료된 정당한 재설정은 동일한 이벤트 유형, 동일한 세부 키, 동일한 code_id를
생성한다. 오직 지연 시간만 다르므로, 판정 전체를 임계값이 떠안게 된다. 서두르지 않는
사람은 누구든 그 임계값 아래에 머문다. 실제 사용자는 임계값을 넘는데, 이는 링크가
흐름을 시작한 브라우저에서 열릴 때만 code_id가 동일하게 유지되기 때문이며, 바로
그 경우에 메일이 이미 열려 있을 가능성이 높다.
우리는 둘을 구분해 주는 이벤트도, 이벤트 세부 정보도, 오류도 찾지 못했다. 찾아볼 "메일 단계 건너뜀" 이벤트는 존재하지 않는다. 존재하는 것은 상태 부작용이며, Q3가 바로 그것을 사용한다.
cve-2026-18963-keycloak-hunt.sql은 여섯 개의 읽기 전용 쿼리를 실행한다:
| 쿼리 | 찾아내는 내용 |
|---|---|
| Q0 | 렐름이 이벤트를 저장하는지 여부, 이벤트 TTL, 그리고 이벤트 세부 정보가 저장되는지 여부. Q2는 code_id 세부 정보를 기준으로 연관 짓기 때문에, 세부 정보가 없으면 Q2는 아무것도 볼 수 없고 그 침묵은 아무 의미도 없다. |
| Q1 | 노출 기간 내에 설정된 모든 비밀번호 자격 증명(credential.created_date). 이것이 바로 계정 탈취 자체이며, 이벤트 만료 후에도 남는다. |
| Q2 | 완료된 모든 자격 증명 업데이트를 동일한 인증 세션의 재설정 메일과 연관 짓고 분류한다. 아래를 참고하라. |
| Q3 | 결정론적 부작용: 정당한 원인 없이 email_verified 상태가 된 사용자. |
| Q4 | Q1과 Q3 행을 설명하고 관리자 주도 경로를 배제하기 위한, Admin-API 자격 증명 재설정 및 사용자 쓰기 기록. |
| Q5 | 단일 사례를 직접 읽어보기 위한 원시 재설정/자격 증명 이벤트 타임라인. |
| 판정 | 의미 |
|---|---|
NO_MAIL_WAS_SENT | 자격 증명 업데이트가 실패한 발송과 code_id를 공유한다: 계정에 주소가 없거나 SMTP가 이를 거부한 경우. 링크가 서버를 떠난 적이 없으므로 아무도 링크를 열 수 없었는데도, 흐름은 여전히 비밀번호 양식에 도달했다. 정당하게는 불가능하므로 익스플로잇으로 간주하라. |
CLEARED_TOKEN_CLICKED | 재설정이 해당 사용자의 어떤 SEND_RESET_PASSWORD와도 다른 code_id로 완료되었다. 다른 브라우저나 세션에서 소비된 액션 토큰만이 이런 결과를 만들 수 있으므로, 메일 링크가 실제로 클릭된 것이다. 결정론적으로 정당하다. |
CANDIDATE | 발송과 동일한 code_id. 이것이 우회의 모습이며, 동시에 정당한 동일 브라우저 재설정의 모습이기도 하다. 그 자체로는 발견 사항이 아니다. |
NOT_RESET_FLOW | 해당 기간 내에 해당 사용자의 SEND_RESET_PASSWORD가 없다. 비밀번호가 다른 방식(계정 콘솔, 관리자 API, 등록)으로 변경된 것이다. Q4와 교차 확인하라. |
secs_after_send는 참고용으로 출력된다. 판정 근거로 사용하지 마라.
취약한 ResetCredentialEmail.action()은 성공하기 전에 계정의 이메일 주소를
인증됨으로 표시한다. 정당한 경로는 action()에 전혀 진입하지 않는데, 이는
authenticate()가 액션 토큰에서 단락 처리되기 때문이며, 따라서 진짜 재설정은
email_verified를 건드리지 않는다. 우리는 26.7.1에서 흐름을 시작한 브라우저와
다른 브라우저 모두에서 완료된 정당한 재설정을 확인했다. 둘 다 미인증 사용자를
미인증 상태로 남겼다. 우회된 재설정은 둘 다 플래그를 뒤집었다.
비밀번호가 해당 구간 내에 변경되었고, 현재 email_verified 상태이며, VERIFY_EMAIL/UPDATE_EMAIL 이벤트도 없고 해당 계정에 대한 관리자 쓰기 작업도 없는 계정은 인증된 것에 대해 무고한 설명이 존재하지 않습니다.
여기서 중요한 한 가지 한계가 있습니다: 해당 구간 이전에 이미 인증된 계정은 Q3에서 보이지 않습니다, 왜냐하면 플래그가 이미 true였기 때문입니다. Q3는 정밀하지만, 완전하지는 않습니다. 가장 강력한 행은 verified_with_no_email = t인데, 주소가 전혀 없는 계정은 재설정 링크를 받을 수도 없고 링크를 열어 인증될 수도 없기 때문입니다.
# defaults: realm 'master', window since 2026-06-01
psql -U keycloak -d keycloak -f cve-2026-18963-keycloak-hunt.sql
# explicit realm and window (run once per realm; plain values, psql quotes them)
psql -U keycloak -d keycloak \
-v realm=myrealm -v since=2026-05-01 \
-f cve-2026-18963-keycloak-hunt.sql
since를 취약한 버전이 배포되기 직전으로 설정하세요. Kubernetes에서는 다음과 같습니다:
kubectl exec -it my-postgres-pod -- \
psql -U keycloak -d keycloak -v realm=myrealm -v since=2026-05-01 \
-f - < cve-2026-18963-keycloak-hunt.sql
먼저 Q0을 확인합니다. events_in_window가 0이거나, 이벤트가 존재하는데 details_with_code_id가 0이라면 Q2는 동작할 수 없으며, 그 침묵은 아무것도 증명하지 못합니다. Q1과 Q3는 테이블 상태를 읽으므로 이벤트 보존 여부의 영향을 받지 않습니다.
그다음 아래로 내려가며 작업합니다:
NO_MAIL_WAS_SENT. 익스플로잇입니다. 이에 대응하십시오.CLEARED_TOKEN_CLICKED. 이 항목들은 정리하고 더 이상 고민하지 마십시오.admin_ops_on_user가 비어 있고 has_verify_email_event = f인 Q3 행은
익스플로잇이며, 단 관리자 이벤트를 기록하지 않는 가져오기 경로를 통해
인증된 것으로 등록된 계정도 여기에 해당한다는 주의 사항이 적용됩니다.CANDIDATE**는 미해결 상태입니다. 데이터베이스만으로는
이를 판정할 수 없으므로 액세스 로그를 확인하십시오.로그가 남아 있는 한 이는 결정적입니다. 이메일로 전송된 링크를 여는 것은 HTTP 요청이며, 이는 우회가 위조할 수 없는 유일한 요소입니다. 리버스 프록시 또는 인그레스 로그에서 정상적인 재설정은 항상 다음과 같이 나타납니다
GET /realms/<realm>/login-actions/action-token?key=...
같은 클라이언트에서, 메일이 발송된 시점과 비밀번호가 설정된 시점 사이에.
grep -E "login-actions/action-token" access.log
Q2의 각 CANDIDATE에 대해, SEND_RESET_PASSWORD와 UPDATE_PASSWORD 사이에서 해당 요청을 찾으십시오. 그곳에 없다면, 메일 게이트를 통과하지 못한 것입니다.
주의할 점이 두 가지 있습니다. action-token에 대한 HEAD 요청은 메일 스캐닝 어플라이언스에서 오며 Keycloak은 이를 무시하므로, GET만 유효합니다. 또한 일부 메일 보안 제품은 링크를 자동으로 따라가는데, 이는 사용자가 하지 않은 실제 GET을 발생시킵니다. 환경에 그러한 제품이 있다면, 요청만 보지 말고 user-agent와 소스 주소로 교차 확인하십시오.
마지막으로, 가장 저렴한 정보원은 종종 사용자 자신입니다. 모든 피해자는 자신이 요청하지 않은 비밀번호 재설정 이메일을 받았습니다. 짧은 기간과 작은 후보 집합이라면, 추론보다 물어보는 것이 낫습니다.
Q1의 행이 자동으로 침해를 의미하지는 않습니다. 각 행을 알려진 정당한 원인(셀프서비스 재설정, 헬프데스크 조치, 신규 등록)과 대조하고, 설명되지 않는 것은 무엇이든 탈취 후보로 취급하십시오.