分析前准备
这里我已经在我自己搭建的域控上创建了一个名为robert
的用户,利用Certipy在AD中利用申请的证书模板进行身份验证
1 | certipy-ad req -u 'robert' -p 'Passw0rd' -ca MY-DC-CA -template User -target 192.168.103.201 |
在默认情况下,域用户可以向证书颁发机构申请注册User证书模板,域计算机可以申请注册Machine证书
利用impacket工具包创建一个机器账户
1 | impacket-addcomputer 'chromos2me.com/robert:Passw0rd' -computer-name 'ROBERTPC' -computer-pass 'Passw0rd' -dc-ip 192.168.103.201 |
新创建的机器账户不存在dNSHostName
属性,我们通过bloodyAD
为机器账户创建dNSHostName
1 | python3 bloodyAD.py -d chromos2me.com -u robert -p Passw0rd --host 192.168.103.201 set object 'CN=ROBERTPC,CN=Computers,DC=Chromos2me,DC=com' dNSHostName -v 'robertpc.chromos2me.com' |
其实也可以通过
impacket-addcomputer
中的-computer-name
参数进行添加,但是我的机器上会显示成功添加,我在域控上查看的时候dNSHostName
并没有成功添加
获取Machine
证书
1 | certipy-ad req -u 'robertpc$' -p 'Passw0rd' -ca MY-DC-CA -template Machine -dc-ip 192.168.103.201 |
进行身份验证
1 | certipy-ad auth -pfx robertpc.pfx -dc-ip 192.168.103.201 |
分析
一般来说两个证书的EKU(Extended Key Usage)这个拓展的证书字段均支持Client Authentication,这个EKU说明颁发的证书可以通过公钥加密( Public Key Cryptography for Initial Authentication (PKINIT))进行Kerberos协议的初始身份验证
为什么ADCS需要给域用户和域内机器账户提供不同的证书模板?
这里我们利用ly4k写的支持PKI版本的Bloodhound去看一下每一张证书的标志位,探究一下不同的账户是如何进行身份验证获取证书
首先是User证书
域用户通过UPN唯一标识自己,我们可以看到证书的Certificate Name Flag字段存在标志位SubjectAltRequireUpn
,在微软官方的文档中记录为CT_FLAG_SUBJECT_ALT_REQUIRE_UPN,此标志指示CA将请求者在AD中用户对象的 UPN (User Principal Name 即我们所说的UPN用户主体名称)属性值添加到颁发证书的主题备用名称(Subject Alternative Name,SAN)扩展中,同时根据微软官方3.1.1.5.1.3 Uniqueness Constraints规定,我们的域内不能存在两个UPN相同的账户,我们可以新建一个用户并将其UPN更改为我们的robert用户的UPN,看看会发生什么
如下图所示,出现了域内一致UPN的冲突
我们接下来回到Machine证书模板,探究一下机器账户是如何进行身份验证的
我们可以看到标志位中存在SubjectAltRequireDns
,在微软文档中记录为CT_FLAG_SUBJECT_ALT_REQUIRE_DNS,此标志指示证书颁发机构CA将从请求者在AD中用户对象的DNS属性获得的值添加到颁发证书的SAN扩展中,我们将刚才申请机器账户证书的截图再拿出来一下
从上图我圈出的地方可以看到,CA通过DNS主机名对机器账户进行验证,我们在AD Explore中看一下这个属性在哪里被定义
如下图所示dNSHostName
中对DNS主机名进行了定义
那么我们能否对其进行修改呢?这里我们需要查看一下robertpc
的安全属性,同时robertpc
这个机器账户是我们通过robert
这个账户进行创建的,我们只观察robert
拥有的权限
关于已验证的到DNS主机名的写入权限的详细解释在Validated-DNS-Host-Name validated writes,简单来说这个权限允许设置与计算机名和域名一致的dNSHostName
属性,似乎有点难以理解,我们接着做一个实验,我们尝试修改dNSHostName
为mytest.chromos2me.com
看看会发生什么
如上图所示被成功修改,同时注意到sAMAccountName
并未被修改
这里的与计算机名和域名一致的
dNSHostName
属性实际上只要我们设置的合理一般都不会出现非法写入
现在能否能利用机器账户进行证书的请求?试试就知道了
很明显我们成功利用新的DNS主机名标识了我们的机器同时进行了证书的申请,这里我们可以进行一个思考,我们在上文提到的Uniqueness Constraints唯一性约束文档中并未提到机器账户的dNSHostName必须唯一,那么能否修改dNSHostName为和域控一样,那么我们是不是就能利用域控身份进行高权限证书的请求呢?
这里因为impacket下的addcomputer在使用默认的SAMR方法创建机器账户时不会包含SPN属性,同时因为我自己的环境问题无法换用LDAPS方法,这里我们就手动的利用krbrelayx中的addspn这个工具进行添加
1 | python3 addspn.py -u chromos2me.com\\robertpc\$ -p Passw0rd -s HOST/robertpc.chromos2me.com 192.168.103.201 |
查询一下SPN
1 | python3 addspn.py -u chromos2me.com\\robertpc\$ -p Passw0rd 192.168.103.201 -q |
根据How to configure SPN微软官方文档所指出的SPN格式
1 | TERMSRV/WSRV2022 |
我们为了复现漏洞环境在域控上添加一条关于主机名的SPN
1 | HOST/robertpc |
实际上在我们刚才使用
addspn
添加SPN时工具自动创建的SPN格式为
1
2
3
4 >KrbRestrictedHost/hostname
>KrbRestrictedHost/hostname.domain_fqdn
>Host/hostname
>Host/hostname.domain_fqdn但是似乎环境存在问题,所以尝试手动复现一下环境,这里可以参考
impacket-addcomputer
中关于创建SPN的代码添加
在域控组织单元中找到DC$
的dNSHostName
对robertpc$
的dNSHostName
修改为域控的dNSHostName
看看会发生什么
点击OK后出现了操作错误
为了追究其原因我们尝试将DNS主机名修改为DC之外的主机名,然后观察一下SPN的变化
由上图发现,dNSHostName
发生变化时,SPN中与FQDN完整限定域名有关的项均发生了改变,可以想象到,当我们将创建的机器账户robertpc$
的dNSHostName
更改为DC.chromos2me.com
时其SPN会变为RestrictedKrbHost/DC.chromos2me.com
和 HOST/DC.chromos2me.com
,这样就会与域控的SPN发生唯一性约束冲突,我们可以看一下域控的SPN属性
到现在为止,我们已经确定了操作错误产生的原因,那么这里的多个SPN属性我们是否能够控制,还是说我们已经没有办法实现DNS主机名的更改了呢,我们看一下robert
用户对robertpc$
的ACL
Validated-SPN validated writes这个权限描述为用于设置符合计算机DNS主机名的SPN属性,和上面的已验证的到DNS主机名的写入权限类似,我们的servicePrincipalName
更新的值也必须符合 dNSHostName
属性,刚才我们注意到我们的四个SPN中只有两个发生了改变,另外两个中并没有包含 dNSHostName
,是不是我们将会发生改变的两个SPN删去后就可以正常进行 dNSHostName
的伪造了?
删去两个变化的SPN
修改DNS主机名
我们尝试利用Machine
模板申请证书,如下图成功申请
继续尝试看其能否利用DC进行身份验证,成功
接下来利用DC哈希执行DCSync攻击转储域控上存储的所有用户哈希
1 | impacket-secretsdump 'chromos2me.com/dc$@dc.chromos2me.com' -hashes aad3b435b51404eeaad3b435b51404ee:a9edd7fbef42767175d43643ae7fcfc7 -target-ip 192.168.103.201 |
证书映射过程
在Oliver Lyak的文章中还提到了在身份验证期间证书如何映射到帐户的技术细节,在此也简单的说一说
首先根据在AS-REQ中指出的形如user@chromos2me.com
的主体名称查找对应的用户,Key Distribution Center(KDC)接着根据账户的userAccountControl 属性,选择不同的映射方式,这里的userAccountControl在我们刚才的机器账户上为4096也就是0x1000,对应WORKSTATION_TRUST_ACCOUNT
即计算机账户,另外域控制器账户的掩码值为0x2000对应SERVER_TRUST_ACCOUNT
,普通用户账户的掩码值为0x0400对应NORMAL_ACCOUNT
,根据不同的账户KDC会通过证书中的SAN字段中的 DNSName 或 UPNName 来验证证书映射,对于上面的三种用户,除了普通账户使用UPNName 字段进行验证外,其他均使用DNSName 字段进行验证
来自3.1.5.2.1.1 SAN DNSName field的微软文档指出KDC 必须确认找到的账户名称与证书中 DNSName 字段中的计算机名(以 $ 结尾)匹配,并且证书中 DNSName 字段中的 DNS 域名与 realm 的 DNS 域名匹配
就拿我们做实验的例子来说,我们的计算机账户robertpc$
,它属于chromos2me.com
,我们为了实现有效的映射,证书中的DNSName应为robertpc.chromos2me.com
,即格式为<计算机名>.<域名>,同时这里的 <计算机名> 是sAMAccountName去掉结尾$后的内容
因此,在 PKINIT Kerberos认证过程中,我们提供的主体名称是类似 robertpc$@chromos2me.com
的格式,并提供一个 DNSName 设置为 robertpc.chromos2me.com
的证书。KDC 会根据主体名称查找对应账户。由于 robertpc$
是一个计算机账户,KDC 会将 DNSName 字段分为计算机名部分和域部分。然后 KDC 会验证计算机名部分是否与 sAMAccountName 加上 $ 后一致,并验证域名部分是否与域名匹配。如果两个部分都匹配,验证就成功了,映射也就成立。值得注意的是,账户的 dNSHostName 属性并不会用于证书映射,该属性仅在申请证书时使用。
修补
该漏洞已通过 Microsoft 2022 年 5 月的安全更新修复,在证书中引入了一个新的对象标识符,通过将用户的SID嵌入到新的 szOID_NTDS_CA_SECURITY_EXT(1.3.6.1.4.1.311.25.2)OID 中,但是具有新CT_FLAG_NO_SECURITY_EXTENSION(0x80000)
标志的证书模板将不会嵌入新szOID_NTDS_CA_SECURITY_EXT OID
,因此这些模板仍然容易受到此攻击,虽然这种标志设置的可能性不大。
About this Post
This post is written by Chromos2me, licensed under CC BY-NC 4.0.