仅凭用户名或电子邮件地址,未认证的攻击者即可在任何 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` | 🟢 **已修复** | 流程转向登录并停留(修复 #51844 已生效) |
| `2` | 🟡 **已缓解** | 重置凭据不可达——*忘记密码* 已关闭。**并非修复补丁。** |
| `3` | ⚪ **无法定论** | 无法识别的响应——**不要将其视为通过** |
按领域(realm)运行——*忘记密码* 是领域级设置,`master` 也适用。
详细说明,以及为什么该检查无需用户且不触碰任何内容,请参阅
[§4a](#4a-safe-detection---safe-check--start-here)。
**已经知道自己受影响?** 跳转到 [修复措施](#8-remediation) 和
[检测 / 威胁狩猎](#9-detection)。
### 在没有目标的情况下试用
该仓库附带一个实验环境,可在同一领域内并排启动易受攻击的 **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/),因此无需触碰任何外部系统即可了解该漏洞的工作原理。 未经授权指向第三方基础设施在大多数司法管辖区 均属违法行为,本项目不支持此类行为。
参考: 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);
该标记是一个**不记录由哪个执行集设置的裸布尔值**。它
仅在处理提交的 `authenticationExecution` 参数的分支中被清除。省略该参数 —— 本 PoC 全程如此 ——
该标志将在整个认证会话期间保持设置。
`processFlow()` —— 当该标志为真时,正常流程评估将被跳过:```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> 写入,并将浏览器跳转到登录页面。被停驻的执行正是电子邮件门。
`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
Six HTTP requests,无需认证,全程没有 authenticationExecution 参数。
model.getId(),并且 processFlow() 仅当它等于
CURRENT_AUTHENTICATION_EXECUTION 时才尊重它,否则将其移除。在攻击场景中两者不同
(choose-user id 与 e-mail-gate id 不同)——这正是该补丁检测到的,也正是
--safe-check 所依据的信号。ResetCredentialEmail.action() 现在要求
context.getUser().getId().equals(authNote(ACTION_TOKEN_USER_ID)),
否则以 INVALID_USER 失败。| 版本线 | 受影响 | 社区修复 |
|---|---|---|
| Legacy(基于 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 引入的,该提交在重置流程中添加了“尝试其他方式”
认证器选择屏幕。更早的版本只是没有该代码路径。这包括旧的基于 WildFly 的发行版
和 RH-SSO 7.x,它们不受此漏洞影响,但已停止维护且易受许多其他漏洞影响。
停留在旧版本并不是一种修复措施。26.7.2 是社区版本线发布的唯一修复版本。
如果部署位于 26.0 – 26.6 上,则该版本线没有补丁版本——修复需要次版本升级,
而不是点版本。26.4.15 / 26.6.6 标签是厂商回移植,不能与社区镜像互换。前提条件: realm 已启用 Forgot password,并且其绑定的重置凭据流程
使用内置的 reset-credential-email 认证器。
该仓库同时提供存在漏洞和已修复的 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 | 受害者的邮箱 |
领域 `poc`,公共客户端 `poc-app`,用户 `victim` / `OriginalPassw0rd!`,Keycloak 管理员 `admin` / `admin`。
使用 `KC_VULN_VERSION` / `KC_PATCHED_VERSION` 固定不同的构建版本:```bash
KC_VULN_VERSION=26.5.7 docker compose up -d keycloak-vuln
在两次运行之间,lab/reset-victim.sh 用于恢复受害者的密码
(KC=http://localhost:8100 lab/reset-victim.sh 针对已修补的实例)。
lab/legit_reset.py 通过从 Mailpit 中提取 action-token 链接并点击它,
执行一次真实的密码重置。它是第 9 节检测工作的对照样本——在同一个 realm 中
分别运行它和漏洞利用脚本,然后对比两者的痕迹。
清理:docker compose down -v。
Python 3.9+,仅使用标准库——无任何依赖,可直接部署到任意跳板机。``` --base Keycloak base URL (e.g. https://sso.example.com) --realm realm name --client-id any enabled public client with the standard flow --redirect-uri a URI permitted by that client (default http://localhost:9999/callback) --insecure skip TLS verification --verbose log every HTTP request --dump FILE write the response body of a failing step to FILE
`--client-id` 可以是任何已启用的、支持标准流程的公共客户端。内置的
`account` 客户端存在于每个 realm 中,是可靠的选择,但它限制了
重定向 URI,因此 `--redirect-uri` **必须**为
`<base>/realms/<realm>/account/` — 默认值会被拒绝,步骤 1 将失败。
### 4a. 安全检测(`--safe-check`)— 从这里开始