Contents
前言
在上文中主要从Kerberos
协议中的AS_REQ
和AS_REP
请求交互中的数据字段以及客户端和服务端的流程进行分析,在TGS_REQ & TGS_REP
阶段,用户通过AS_REP拿到的TGT票据,去向KDC申请特定服务的访问权限,KDC校验TGT票据,如果校验通过的话,会向用户发送一个TGS票据,之后用户再拿着TGS去访问特定的服务。这一阶段,微软引进了两个扩展S4U2SELF和S4U2PROXY
TGS_REQ
来看一下TGS_REQ
阶段的数据包:

1. msg-type
类型,TGS_REQ对应的就是
KRB_TGS_REQ(0x0c)
,对应的十进制值为122. PA-DATA
在
PA-DATA
主题部分存在几个较为关键和重要的字段– AP-REQ
这个是在
TGS_REQ
中必须要携带的字段,因为在ap-req
是之前AS-REP
得到的TGT的票据结构,此时用来请求TGSsname:该票据所属的服务端的身份
enc-part:用KDC的密钥加密的票据部分,可以看到和
AS-REP
中返回的ticket->enc-part
是一样的authenticator这里是经Logon Session Key加密的Authenticator,也是属于在下一步认证使用的会话密钥
- PA_FOR_USER
在数据包中使用PA-FOR-USER
数据结构,利用客户端用户名和域名称来向KDC标识用户:
- PA_PAC_OPTIONS
类型是 PA_PAC_OPTIONS,S4U2proxy则必须扩展PA-PAC-OPTIONS
结构。
如果是基于资源的约束委派,就需要指定Resource-based Constrained Delegation位
3. req-body

sname是指请求的服务名称,这里操作是以域用户的身份登录主机,因此请求的服务名称就是域主机名,有个比较有意思的特性是,如果指定的服务是krbtgt,那么拿到的TGS票据是可以当做TGT票据用的。
TGS_REP
还是先看下TGS_REP
的数据包:

1. msg-type
AS_REQ
的响应body对应的就是KRB_TGS_REQ(0x0d)
,对应的十进制值是132. ticket
在
TGS_REP
中的ticket
实际上就是一张ST(Service Ticket),也被称为TGS。相对于在AS_REP
中的Ticket而言,这里的enc_part
是通过服务账户的Hash值进行加密得到的,而在AS_REP
中是使用krbtgt用户Hash得到的,在我们已知krbtgt用户的Hash时可以伪造一张TGT,这里的TGT成为黄金票据,而在TGS_REP
中如果我们得到服务账户的Hash值,就可以使用该hash伪造一张ST/TGS,从而去访问对应的服务,这就是票据传递攻击(Pass The Ticket)
中的白银票据
这里也可以反向操作,当我们去请求服务的ST/TGS时,从里面的enc_part
中得到服务账户的Hash时并且进行爆破得到明文时,这种攻击方式成为Kerberoasting攻击
3. enc_part
这里的enc_part
是通过上一阶段AS_REP
中通过用户账户的Hash加密得到的LOGON SESSION KEY
加密得到的SERVICE SESSION KEY
是用于请求服务时的会话密钥,换句话说这部分同样是可以解密的,key是上一轮AS_REP
里面返回的session_key,也就是导入凭据里面的session_key,解密后得到encryptionkey
这个结构里面最重要的字段也是session_key
(不同于上一轮里面的session_key),用来作为作为下阶段的认证密钥。
S4U
S4U协议是微软为Kerberos做的扩展协议,主要有S4U2self
和S4U2proxy
,应用在约束委派中
还是用一张之前学习约束委派攻击的图来说明:

S4U2self
S4U2self扩展允许服务Service 1
代表特定的某用户User 1
向KDC请求该服务的可转发票据TGT
用户会通过其他协议(例如NTLM或基于表单的身份验证)对服务进行身份验证,因此他们不会将TGS发送给服务。在这种情况下,服务可以调用
S4U2Self
来要求身份验证服务为其自身的任意用户生成一个TGS,然后可以在调用S4U2Proxy时将其用作凭证
这里面很重要的一点是服务代表用户获得针对服务自身的kerberos票据这个过程,服务是不需要用户的凭据的

S4U2self过程如上图所示,不过需要注意的是在这里虽然服务不需要用户的凭据,但是在向KDC请求可转发的TGS时是需要KDC通过TGT验证的,也就是说需要合法的TGT才能够通过S4U2self请求获得
可转发TGT
注意在这里就引出了非约束用户的概念,当发出请求的用户的userAccountControl
中设置了TRUSTED_TO_AUTH_FOR_DELEGATION
字段时此时该用户对应就是非约束用户,服务可以对该用户使用S4U2self,代表该用户向KDC发起对本服务的TGS可转发票据的请求
对应数据包中的PA-FOR-USER
字段,利用该字段向KDC说明S4U2self的对象用户和域名城:

从请求的数据包中也能看出请求的是可转发的TGS:

S4U2Proxy
S4U2proxy要求Service 1
的服务票据设置了可转发标志(即通过S4U2self获得的服务票据),然后向msds-allowedtodelegateto
中指定的服务请求服务票证,换句话说,s4u2proxy使得服务1可以使用来自用户的授权,这是通过S4U2self阶段获得的,然后用获得的TGS再向KDC请求服务2的TGS,即代表用户访问服务2,并且只能够访问服务2
我们通过daiker
师傅提供的图来看下:

注意:通过S4U2self获得的服务票据(Service 1的TGS)会放在数据包中的additional-tickets来向KDC请求
Service 2
的TGS
这里请求的为域内Win 7
主机的CIFS服务:

注意:S4U2Self返回的票据是可转发的TGS,这个票库作为在S4U2PROXY中的additionTicket,除了资源约束委派情况,如果该TGS是不可转发的,S4U2proxy仍然可以进行,但生成的TGS是不可转发的
因此可以分为如下两种情况讨论:
– 1.AddtionTicket里面的票据是可转发的
这样就意味着通过S4U2Self
向KDC申请的服务票据是可转发的TGS,那么只要在KDC Options
里面置forwarable位,则通过S4U2PROXY向服务2申请的TGS也同样是可以转发的
- 2.AdditionTicket的票据设置为不可转发
当TGS的票据是不可转发时,S4U2Proxy仍然能够进行,但是生成的TGS同样也是不可转发的,这一点前文已经说明,但是也有一个例外,也就是出现资源约束委派的时候
这里直接用daiker
师傅总结的描述:当配置了服务1到服务2的基于资源的约束委派,且PA-PAC-OPTION设置了Resource-Based Constrained Delegation标志位
(这一例外的前提是S4U2SELF阶段模拟的用户没被设置为对委派敏感,对委派敏感的判断在S4U2SELF阶段,而不是S4U2PROXY阶段)
返回的TGS票据仍然是可转发的
委派
委派的概念在这里不在过多叙述,在之前的文章已经学习了针对非约束委派和约束委派的攻击方式,在这里为了文章的连贯性,在简单叙述下约束委派和非约束委派的概念。
约束委派和非约束委派
非约束委派,当一个服务账户被设置为非约束的委派,那么该服务账户可以接受任何用户的委派去向任意的服务进行请求,这个用户去委派该服务账户去访问某个服务的时候,该用户会将TGT发送到服务账户并且会驻存在LSASS内存中,方便后续使用,而该服务账户类似中间人,替代模拟委托用户申请访问其他服务
注:配置非约束委派的用户userAccountControl属性有标志位:TrustedForDelegation
约束委派,基于非约束委派的不安全性,在 Windows 2003上发布了”约束”委派。其中包括一组Kerberos协议扩展,就是本文之前提到的两个扩展 S4U2Self
和S4U2Proxy
。当服务用户配置为约束委派时,那么该服务用户能够接受任何用户的委派但是只能访问特定的服务,并且该用户的TGT是不会发送给服务用户的,这个过程中就包含了S4U2SELF和S4U2PROXY两个过程,首先代表用户获得针对服务自身的可转发的kerberos服务票据(S4U2SELF)
,拿着这个票据向KDC请求访问特定服务的可转发的TGS(S4U2PROXY)
,并且代表用户访问特定服务,而且只能访问该特定服务。
基于资源的约束委派
因为配置受约束的委派,必须拥有SeEnableDelegation
特权,该特权是敏感的,通常仅授予域管理员。为了使用户/资源更加独立,Windows Server 2012
中引入了基于资源的约束委派。
基于资源的约束委派(RBCD)配置在后端目标服务或资源上(例如后端的 CIFS 服务),而不是在前端的服务或资源上(例如前端的 WEB 等)。基于资源的约束委派允许资源配置受信任的帐户委派给他们。
在这里可以发现其实基于资源的约束委派和约束委派大体上是非常相似的,区别在于作用的方向,先借一张图:

这张图其实表达的非常明显,在传统的约束委派中,是在ServiceA
的msDS-AllowedToDelegateTo
属性中配置了对ServiceB的信任关系,定义了到 ServiceB的传出委派信任。
注意在传统的约束委派中,通过S4U2SELF获得的TGS票据一定是可转发的,否则S4U2PROXY会失败
而在资源约束委派中,是在ServiceB
的msDS-AllowedToActOnBehalfOfOtherIdentity
属性中配置了对ServiceA的信任关系,定义了从ServiceA的传入信任关系。资源本身可以为自己配置资源委派信任关系,资源本身决定可以信任谁,该信任谁。
TGS_REQ&TGS_REP过程涉及的安全问题
PTK(pass the ticket)
和PTH
类似,在Kerberos
协议中只有AS_REQ
过程使用用户Hash加密时间戳验证,而在其他阶段的验证均是通过票据(TGS票据和TGT票据),而票据字段主要是通过session_key和ticket字段(使用服务账户Hash加密)进行验证,因此我们拿到票据之后就可以通过票据作为访问服务或者是进行下一阶段的认证
kerberosting攻击
前文中提到在TGS_REP
中存在enc_part
部分,注意是在ticket
字段内的enc_part
,这一部分是通过服务账户的Hash进行加密得到的,而在外部的enc_part
是通过上一阶段AS_REP
中的session_key加密得到的,如果我们拿到这一部分的票据时我们可以通过爆破服务账户Hash的方式来得到其明文密码
但是在这里利用kerberosting攻击意味着我们必须要有可转发的TGT,因为在TGS_REQ
中所用的凭证是在AS_REP
拿到的krbtgt用户Hash加密的TGT
根据Kerberos
协议,任何用户均可以向域服务器申请访问某个服务,服务可以不在线,只要该服务在域中注册了SPN(Service Principal Name)
即可。
SPN是服务器上所运行服务的唯一标识,每个使用Kerberos
的服务都需要一个 SPN。SPN分为两种,一种注册在AD上机器帐户(Computers)下,另一种注册在域用户帐户(Users)下。
当一个服务的权限为Local System
或Network Service
则SPN注册在机器帐户(Computers)下,当一个服务的权限为一个域用户,则SPN注册在域用户帐户(Users)下
SPN 的格式为 serviceclass/host:port/servicename
其中serviceclass表示服务的种类,例如 www 表示web服务;端口如果是知名端口,可以省略。
针对上述内容,我们可以总结经典的Kerberoasting攻击姿势:
1. 使用GetUserSPNS.ps1脚本或者Sean的Find-PSServiceAccounts.ps1脚本或PowerView的“Get-NetUser -SPN”来枚举域帐户的SPN。
- 请求这些特定的SPN的 TGS可以使用Windows内置的工具setspn.exe或者在PowerShell中调用.NET的
System.IdentityModel.Tokens.KerberosRequestorSecurityToken
类。
One ticket
PS C:\> Add-Type -AssemblyName System.IdentityModel
PS C:\> New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList "HTTP/web01.medin.local"
All the tickets
PS C:\> Add-Type -AssemblyName System.IdentityModel
PS C:\> setspn.exe -T medin.local -Q */* | Select-String '^CN' -Context 0,1 | % { New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList $_.Context.PostContext[0].Trim() }
- 使用Mimikatz的kerberos::list/export命令从内存中提取这些票证,并设置可选的base64导出格式。然后下载票据,或者将base64编码的票证拖到攻击者的机器上进行解码。
-
使用tgsrepcrack.py开始离线破解密码,或者使用
John the Ripper
的kirbi2john.py从原始票证中提取可破解的哈希格式。
此外,在这篇文章中还提到了Kerberoasting攻击的新姿势,大致是通过使用的.NET类KerberosRequestorSecurityToken
中有一个GetRequest
的方法,它返回Kerberos服务票证的原始字节流,使用位字符串操作,能够轻松地提取加密的TGS(即可破解的哈希)
···
白银票据
前文说到在TGS_REP
里出现的ticket
中的enc_part
部分就是使用申请服务账户的Hash值进行加密,当我们拥有服务账户的Hash时,可以伪造签发任意用户申请该服务的TGS票据,这个票据称为黄金票据。相较于黄金票据,白银票据使用要访问服务的hash,而不是krbtgt的hash,由于生成的是tgs票据,因此是不需要和域控主机进行通信的,但是白银票票据只能访问特定服务
pac协议
考虑到安全性的问题,微软为Kerberos添加了一个扩展PAC,那么在这里主要学习pac相关过程以及PAC在历史上出现过的一个严重的缺陷,该缺陷利用导致允许普通域用户提升到域管的漏洞 MS14068。
我们不妨回顾下在整个Kerberos协议
体系下,那个阶段中我们使用了pac,没错就是在AS_REP
返回的TGT中包含的PAC,而PAC包含Client的sid以及Client所在的组。因此其实在这里我们也就知道了PAC是微软为了实现访问控制而引出的扩展。因为要判断用户有没有权限访问该服务,因此最后服务受到用户请求的TGS后会将其在发送给KDC进行解密后得到用户的sid和所在组来判断是否具有访问权限。
因此重新梳理下有PAC穿插的整个Kerberos协议:
1. 用户向KDC发起AS_REQ
,请求凭据使用用户hash加密的时间戳,KDC用对应用户的Hash解密,结果正确返回则用krbtgt hash
加密的TGT票据,TGT里面包含PAC,PAC包含用户的sid,用户所在的组。
- 用户凭借TGT票据向KDC发起针对特定服务的TGS_REQ请求,KDC使用krbtgt hash进行解密,如果结果正确,就返回用服务hash 加密的TGS票据
注意:在这一阶段是不会判断用户是否具有访问权限的,只要提供的TGT正确,就会返回使用服务账户Hash加密的TGS票据,因此任何一个用户,其hash我们知道,就可以请求域内任何一个服务的TGS票据,然后离线爆破
- 用户使用TGS票据在向想要访问的服务进行请求,服务使用服务账户Hash解密成功后,会将其中的PAC在发送给KDC,解密得到PAC中用户所在的sid和组,再判断用户是否有访问服务的权限,有访问权限
注意:在此阶段中不是所有服务都会存在验证PAC,这也是白银票据能够成功的前提,因为即使使用服务账户Hash生成TSG后如果还需要验证PAC,PAC无法伪造还是无法通过KDC验证,则无法申请访问服务
这里跟着daiker
师傅看看PAC的结构:
PAC整体的结构上是一个AuthorizationData
的结构:
AuthorizationData ::= SEQUENCE OF SEQUENCE {
ad-type [0] Int32,
ad-data [1] OCTET STRING
}
AuthorizationData结构的ad-type主要有以下几个:

如上图所示,整个PAC最外层的ad-type为AD-IF-RELEVANT,ad-data还是一个AuthorizationData结构。
这个AuthorizationData的ad-type为
AD-WIN2K-PAC
,ad-data为一段连续的空间,其中包含了若干个PAC_INFO_BUFFER
,这是一个key-value
类型的键值对,其中就包含了用户信息和组信息等
- KERB_VALIDATION_INFO
这个结构是登录信息,也是整个PAC最重要的部分,整个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; //用户的sid
ULONG PrimaryGroupId;
ULONG GroupCount;
[size_is(GroupCount)] PGROUP_MEMBERSHIP GroupIds;//用户所在的组,如果我们可以篡改的这个的话,添加一个500(域管组),那用户就是域管了。在ms14068 PAC签名被绕过,用户可以自己制作PAC的情况底下,pykek就是靠向这个地方写进域管组,成为使得改用户变成域管
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;
类似于PAC这种验证证书肯定会存在签名来保证其完整性和实现防篡改的,微软给PAC设计了两个签名,分别是服务校验和与KDC校验和
微软官方文档

服务校验和使用服务端的密码加密,KDC校验和使用KDC的密码加密
最终加密PAC的密钥被放在Authenticator的subkey中,服务端使用该密钥解密出PAC然后再进一步验证
ms14-068分析
微软对PAC的设计上,签名使用的是HMAC系列中的checksum算法,当我们没有krbtgt密钥时是无法进行伪造PAC的。
而在实际情况中,Windows却允许使用任意算法来计算校验和,而且这个加密算法是由客户端来指定的,KDC会根据客户端指定的算法来验证签名,那我们只需要把PAC
进行md5,就生成新的校验值,也就意味着我们可以随意更改PAC的内容,完了之后再用md5 给他生成一个服务检验和以及KDC校验和,这样就可以就可以任意修改PAC,并且还能通过KDC的认证。
在之前PAC
的结构体中便可以修改PGROUP_MEMBERSHIP GroupIds
为500,这样标志该用户为域管理员组
因此ms14-068利用的本质其实是Microsoft Windows Kerberos KDC
无法正确检查Kerberos票证请求随附的特权属性证书(PAC)中的有效签名
这里借loong716
师傅的图通过MS14-068利用的流量包来看一下过程:

这里的
AS_REQ
和一般正常的Kerberos请求来说其申请获得一张不含有PAC的TGT,也就是说include-pac
字段设置为了False

在ms14-068的EXP中也同样配置了该选项:

在之后的操作之前我们首先还需要了解在KERB_VALIDATION_INFO
结构里面,有这两个字段,UserId和GroupIds这两个字段,其中GroupId是用户所在的组,那只要我们把重要组(比如域管组)的sid加进GroupId即可进行身份的伪造
在pykek加入的是以下组:
域用户(513)
域管理员(512)
架构管理员(518)
企业管理员(519)
组策略创建者所有者(520)
在重构PAC的函数中我们发现存在_build_pac_logon_info
函数:

跟进该函数发现:

这个过程的作用就是构造用户的Group SID,可以看到有512、513、518、519、520这几个,微软官方对这些SID定义的组也就是上文所列的,因此原理就是使用高权限的组的sid加入到PAC中的GroupId字段使得
KDC
将伪造的身份误以为是真实身份
但是我们还是遇到了一个问题,因为在AS_REP
中生成的PAC是包含在TGT内的,而TGT又是使用krbtgt用户的Hash
加密生成的,那我们如何将伪造的PAC填充到TGT内呢?
让我们先从poc的代码中分析下:
#TGS_REQ阶段
subkey = generate_subkey()
nonce = getrandbits(31)
current_time = time()
pac = (AD_WIN2K_PAC, build_pac(user_realm, user_name, user_sid, logon_time))
tgs_req = build_tgs_req(user_realm, 'krbtgt', target_realm, user_realm, user_name,
tgt_a, session_key, subkey, nonce, current_time, pac, pac_request=False)
这里是随机生成了一个16位的subkey
,随后pac
进入了build_tgs_req函数
,继续跟进
def build_tgs_req(target_realm, target_service, target_host,
user_realm, user_name, tgt, session_key, subkey,
nonce, current_time, authorization_data=None, pac_request=None):
if authorization_data is not None:
ad1 = AuthorizationData()
ad1[0] = None
ad1[0]['ad-type'] = authorization_data[0]
ad1[0]['ad-data'] = authorization_data[1]
ad = AuthorizationData()
ad[0] = None
ad[0]['ad-type'] = AD_IF_RELEVANT
ad[0]['ad-data'] = encode(ad1)
enc_ad = (subkey[0], encrypt(subkey[0], subkey[1], 5, encode(ad)))
else:
ad = None
enc_ad = None
req_body = build_req_body(target_realm, target_service, target_host, nonce, authorization_data=enc_ad)
chksum = (RSA_MD5, checksum(RSA_MD5, encode(req_body)))
authenticator = build_authenticator(user_realm, user_name, chksum, subkey, current_time)#, ad)
ap_req = build_ap_req(tgt, session_key, 7, authenticator)
可以看到在这里poc作者是将pac
放置在了TGS_REQ
的authorization_data
字段,该字段是位于req_body
部分的,并且将subkey
放到了TGS_REQ
的authorization
中

结合流量其实就是这样:

因此大体流程就是KDC拿到AP_REQ之后,提取里面authenticator
的密文,用session_key
解密获得subkey,再使用subkey解密enc-authorization-data
获得PAC.而PAC是我们自己伪造的.
因此Ms14-068利用的整体流程其实总结起来非常的简介:
- 发起一个PA_PAC_REQUEST里面选择
include_pac
为false的AS_REQ
的请求,此时KDC返回的TGT票据是不含有PAC的 - 我们伪造一个PAC。sid为当前用户的sid。将如下组的sid加进GroupId:
域用户(513)
域管理员(512)
架构管理员(518)
企业管理员(519)
组策略创建者所有者(520)
并且该PAC我们不是填充到TGT中,而是填充到TGS_REQ
中的enc-authorization-data
中,并且使用subkey加密,而subkey则放到authorization
,等待KDC用session_key
进行解密然后在通过这个subkey解密在authorization-data
中的pac
但是不知道为何把这样放置PAC之后KDC还是依然能够解密PAC,这里原因不详,最后KDC把PAC中的User SID、Group SID
取出来,重新使用进行签名,签名算法和密钥与设置inclue-pac
标志位为TRUE时一模一样。将新产生的PAC
加入到解密后的TGT中,再重新加密制作全新的TGT发送给Client
最后利用的话可以直接使用pykek
写的python的exp
python ms14-068.py -u user-a-1@dom-a.loc -s S-1-5-21-557603841-771695929-1514560438-1103 -d dc-a-2003.dom-a.loc
在kekeo
中也集成了ms14-068
的攻击模块
exploit::ms14068 /domain:域名 /user:域用户名 /password:用户明文 /sid:域用户sid /ptt

在MSF中也有对应的模块进行利用:
