证书模板是 AD CS 里最容易被忽略的提权入口。域内任何普通用户只要发现某个模板勾了「在请求中提供使用者名称」,就能给自己签一张「域管理员」身份的证书,再用它做 PKINIT 登录,整个过程不需要一个提权漏洞,全走微软文档里的合法流程。
本文从攻防视角梳理 ESC1 到 ESC8 的成因,给出证书模板的安全配置、CA 策略标志修正、Web 注册端点加固与审计落地,每一步都附可执行的命令与验证方法。目标不是学会打,而是把这条路径在自己域里彻底堵死。
适用场景
只要域内装过 AD CS(企业根 CA、证书颁发机构、NDES、SCEP),下面三种情况就必然存在:
- 红队常规起手式:拿到一个普通域账号后的第一件事往往是跑扫描器找可滥用模板。这条路比找域内提权漏洞稳定得多,因为它是配置问题,不是代码缺陷,永远不会被补丁修掉。
- 勒索软件横向扩权:攻击链里「普通账号 → 域管」的跳跃点,证书提权是目前最省事的一种。拿到域管就等于拿到全域组策略下发权,加密可以一次性铺满。
- 合规检查项:等保与内网安全基线都把 PKI 基础设施的权限收敛列为检查点,模板 ACL 与 CA 策略标志是必查内容。
反过来说,只用公有云证书、域内没有企业 CA 的环境不涉及本文内容,去做只会浪费人力。
前置条件
- Windows Server 2016 及以上,已部署 AD CS 企业 CA;企业 CA 与独立 CA 的差异在于模板由 AD 存储,模板才可能被滥用。
- 具备域管理员或 CA 管理员权限,能改模板 ACL、改注册表策略标志、重启
CertSvc服务。 - 一台已加入域的 Windows 管理主机,安装 RSAT 的 AD PowerShell 模块;可选装 PSPKI 模块用于批量读模板。
- 一个只读域账号作为复查账号。它能看到什么,攻击者就能看到什么,这是检验加固效果最直接的口径。
- 变更窗口:重启 CertSvc 会中断证书颁发约 10 到 30 秒,在线业务需要提前通知。
原理说明
证书登录为什么等于身份
域内用证书认证走的是 PKINIT:客户端拿证书私钥签名,KDC 校验证书链与 UPN/SAN 映射,通过后直接签发 TGT。也就是说,证书里的身份字段决定了你会变成谁。只要能让 CA 给你签一张域管 UPN 的证书,你就拿到了域管票据,不需要知道域管密码。
这条链路里 CA 是唯一的信任根。CA 说你是域管,KDC 就认,模板只是 CA 签发时套用的一张规则表。
ESC1:模板允许请求者自带 SAN
模板里 msPKI-Certificate-Name-Flag 的 0x1 位(CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT)一旦打开,请求者可以在申请时自行填写使用者名称。再叠加「客户端身份验证」EKU 和对普通用户可注册的权限,就构成了 ESC1。这一条占了实际滥用案例的绝大多数。
同类变体是 CA 级别的 EDITF_ATTRIBUTESUBJECTALTNAME2 标志(值 0x40000)。它比模板级更危险:对所有模板生效,连原本安全的模板也会被拖下水,这个标志常被早年按网上教程配置 SCEP 时顺手打开,然后一直留着。
ESC2 到 ESC5:不是模板问题,是权限问题
ESC2 是模板的 EKU 为「任何用途」,签出来的证书可以冒充任何服务;ESC3 是注册代理模板被滥用,持代理证书可替他人申请;ESC4 是模板对象的 ACL 里给了普通用户写权限,攻击者直接把模板改成 ESC1 形态;ESC5 是 CA 对象、CA 服务器本机的 ACL 过宽,改配置或直接读 CA 私钥。这四条的共同点是要查 AD 对象 ACL,而不只是看模板勾选项。
ESC6 与 ESC8:CA 标志与中继
ESC6 就是前面说的 EDITF_ATTRIBUTESUBJECTALTNAME2。ESC8 走的是另一条路:CA 的 Web 注册端点 /certsrv 默认允许 NTLM 认证,攻击者把一台机器的认证中继到该端点,就能替那台机器(包括域控)申请证书。它的前提是能发起 NTLM 认证且端点允许中继,防御手段是关掉 NTLM 或启用扩展保护(EPA)。
操作步骤
步骤一:用只读账号做一次全面扫描
先摸清现状再动手。certipy 的 find 子命令只做 LDAP 查询,不改任何东西,用只读账号跑最安全。输出里出现 ESC1 之类标记的模板就是待处理清单。
# 只读账号即可,切勿用域管跑,避免误操作
certipy find -u audit@corp.local -p 'ReadOnly#2026' -dc-ip 10.0.0.10 \
-vulnerable -stdout -text
# 只要模板清单,不要漏洞判定
certipy find -u audit@corp.local -p 'ReadOnly#2026' -dc-ip 10.0.0.10 -stdout | head -80
步骤二:枚举所有模板的 SAN 标志与 EKU
不依赖第三方工具也能查,用 AD 模块直接读 Configuration 分区。NameFlag 出现 0x1 就要重点看,再对照 pKIExtendedKeyUsage 是否含客户端身份验证(1.3.6.1.5.5.7.3.2)。
$cfg = (Get-ADRootDSE).ConfigurationNamingContext
$base = "CN=Certificate Templates,CN=Public Key Services,CN=Services,$cfg"
Get-ADObject -SearchBase $base -LDAPFilter "(objectClass=pKICertificateTemplate)" `
-Properties msPKI-Certificate-Name-Flag, pKIExtendedKeyUsage, msPKI-Enrollment-Flag |
Select-Object Name,
@{n='NameFlag'; e={'0x{0:X}' -f $_.'msPKI-Certificate-Name-Flag'}},
@{n='ClientAuth'; e={($_.'pKIExtendedKeyUsage' -contains '1.3.6.1.5.5.7.3.2')}},
@{n='ManagerApproval'; e={[bool]($_.'msPKI-Enrollment-Flag' -band 0x2)}} |
Sort-Object Name | Format-Table -AutoSize
步骤三:关掉 CA 级别的 SAN 任意指定
这一步优先级最高,因为它影响全部模板。命令里的减号是「清除该标志」的语义,漏掉减号会变成再次打开,方向完全相反。改完必须重启服务才生效。
certutil -setreg policy\EditFlags -EDITF_ATTRIBUTESUBJECTALTNAME2
net stop certsvc
net start certsvc
# 验证:输出中不应再出现 0x40000
certutil -getreg policy\EditFlags
步骤四:修正模板 ACL,只留指定组可注册
模板对象的安全描述符决定了谁能读、谁能注册、谁能改。Authenticated Users 的「注册」权限是 ESC1 能够成立的前提,只要撤销它、改成专门的业务安全组,即使模板还留着 SAN 标志,普通账号也签不出证书。同一批命令顺带检查是否有人对模板拥有 WriteProperty(对应 ESC4)。
$tplDn = "CN=WebServer,CN=Certificate Templates,CN=Public Key Services,CN=Services,$cfg"
$entry = [ADSI]"LDAP://$tplDn"
# 列出所有非继承的显式权限,重点看 Enroll / WriteProperty / WriteDacl
$entry.psbase.ObjectSecurity.Access |
Where-Object { -not $_.IsInherited } |
Select-Object IdentityReference, ActiveDirectoryRights, AccessControlType |
Format-Table -AutoSize
# 移除 Authenticated Users 的注册权限(保留读权限)
$rule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
(New-Object System.Security.Principal.SecurityIdentifier('S-1-5-11')),
[System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight,
[System.Security.AccessControl.AccessControlType]::Deny,
[Guid]'0e10c968-78fb-11d2-90d4-00c04f79dc55')
$entry.psbase.ObjectSecurity.AddAccessRule($rule)
$entry.psbase.CommitChanges()
改完模板 ACL 后,必须在 CA 上重新发布一次模板,否则 CA 服务缓存里还是旧权限,注册行为不会变化。重新发布不会吊销已有证书,只让 CA 重新读取模板定义。
# 在 CA 服务器上执行:移除再添加即触发 CA 重新加载模板
certutil -SetCATemplates -WebServer
certutil -SetCATemplates +WebServer
# 查看 CA 当前已发布的模板列表
certutil -CATemplates
步骤五:封堵 Web 注册端点的 NTLM 中继
ESC8 的入口是 IIS 上的 /certsrv 虚拟目录。两条路:不用 Web 注册就直接卸载证书注册 Web 服务角色;必须用就启用 SSL 与扩展保护,并把 NTLM 从该站点摘掉。
# 方案 A:不再需要网页注册时,直接移除该角色(推荐,攻击面直接消失)
Uninstall-WindowsFeature -Name ADCS-Web-Enrollment -Restart
# 方案 B:保留端点,启用 SSL + 扩展保护强制令牌检查
Import-Module WebAdministration
Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' `
-Location 'Default Web Site/CertSrv' `
-Filter 'system.webServer/security/access' -Name 'sslFlags' -Value 'Ssl,SslRequireCert'
& "$env:windir\system32\inetsrv\appcmd.exe" set config "Default Web Site/CertSrv" `
/section:windowsAuthentication /extendedProtection.tokenChecking:"Require" /commit:apphost
步骤六:打开 CA 审计并接入日志
加固之后仍需能发现「有人在尝试」。CA 侧有两类可用证据:Certification Services 审计子类别下的事件 4886(已颁发)与 4887(已拒绝),以及证书模板变更的目录服务访问事件。
# 开启 CA 审计筛选器(127 覆盖颁发、吊销、配置变更等主要事件)
certutil -setreg CA\AuditFilter 127
net stop certsvc
net start certsvc
# 开启系统级「认证服务」审计子类别
auditpol /set /subcategory:"Certification Services" /success:enable /failure:enable
auditpol /get /subcategory:"Certification Services"
# 快速查看最近被拒绝的申请,失败量突增往往意味着有人在扫模板
wevtutil qe Security /q:"*[System[(EventID=4887)]]" /c:20 /f:text
配置验证
加固是否真生效,用只读账号复扫一遍最直接。若仍有 ESC 标记,说明对应模板的 ACL 没改到位或 CA 没重新加载。
# 1) CA 策略标志复查,输出中不应出现 0x40000
certutil -getreg policy\EditFlags
# 2) 模板标志复查,SAN 位应为 0x0
Get-ADObject -SearchBase $base -LDAPFilter "(objectClass=pKICertificateTemplate)" `
-Properties msPKI-Certificate-Name-Flag |
Select-Object Name, @{n='NameFlag'; e={'0x{0:X}' -f $_.'msPKI-Certificate-Name-Flag'}} |
Where-Object { $_.NameFlag -ne '0x0' }
# 3) 用普通账号做正向验证:申请带自定义 SAN 的证书应当被拒绝
certreq -new -q request.inf request.req
# 预期报错包含 0x80094800 或策略模块拒绝,说明 SAN 指定已关闭
# 4) 攻击面复扫
certipy find -u audit@corp.local -p 'ReadOnly#2026' -dc-ip 10.0.0.10 -vulnerable -stdout
这里最值得信任的是第 3 步:工具判定的规则可能滞后,而实际签不出证书是硬结论。建议把这条正向测试写成变更后的固定验收动作。
常见问题
Q1:模板改完了,之前签出去的证书还能用吗
能用。改模板只影响新申请,不影响已签发证书的有效性,那些证书在有效期内依旧携带旧的 SAN,照样可以 PKINIT 登录。所以处置必须补两步:吊销所有由该模板签发的证书并发布新的 CRL,同时清理域对象上遗留的证书映射(altSecurityIdentities 属性里手工绑定的证书),否则等于门锁换了、钥匙还在别人手里。
吊销后要验证 CRL 真的分发了:certutil -CRL 重新发布,再在客户端用 certutil -URL 检查 CRL 分发点可达。很多环境 CRL 发布正常但分发点走 HTTP 被防火墙挡了,客户端拿不到新 CRL,吊销形同虚设。
Q2:业务确实需要自定义使用者名称,怎么在不开放 ESC1 的前提下实现
把「谁可以指定 SAN」从所有请求者收窄到少数代理。标准做法是签发一批注册代理证书(注册代理 EKU),由代理替业务申请,模板上只给代理组注册权限;申请时通过 RequestedBy 字段声明实际主体。另一条路是给模板打开 CA 管理员审批(msPKI-Enrollment-Flag 的 0x2 位),申请进队列,人工核对后再批。
两条路都比「开放在请求中提供」安全得多:前者把权限收窄到具体身份,后者把权限收窄到人工流程。真正要避免的是「为了省事全开,反正内网」,内网横向移动最喜欢这种环境。
Q3:改模板或改 ACL 之后,提交申请报 0x80070005 拒绝访问
先确认改动方向:如果只是撤掉了 Authenticated Users,而业务组又不在模板 ACL 里,业务自然申请失败。用 certutil -template 打印模板详情,核对 msPKI-Certificate-Name-Flag 与 ACL 中目标组的 Read + Enroll 两项权限,缺一不可。
另一个高频原因是 CA 服务账户(CertSvc 运行身份)对模板对象没有读权限。CA 读取模板是以自己的服务身份发起的,不是以请求者身份,所以给业务组权限的同时要保留 CA 计算机账户的读权限。改完记得 net stop certsvc 再启动,并用 certutil -CATemplates 确认模板仍在已发布列表中。
总结
AD CS 提权的本质是「CA 愿意替谁签什么身份」,加固就是把这三个变量各自锁死:
- 签给谁:模板 ACL 撤销 Authenticated Users 的注册权,改为专用安全组,顺带排查 WriteProperty 造成的 ESC4。
- 签什么身份:关闭模板 SAN 标志与 CA 级
EDITF_ATTRIBUTESUBJECTALTNAME2,需要自定义名称时走代理或审批。 - 怎么申请:封堵 Web 注册端点的 NTLM 中继,不用就卸角色,要用就上 SSL 与 EPA。
最后补一条容易被跳过的动作:用普通账号做正向验证。配置改完不等于生效,CA 缓存、模板未重新发布、ACL 继承冲突都会让改动落空。拿一个普通账号真去申请一次带自定义 SAN 的证书,被拒绝才算真正堵住。