在安全客拜读了daiker师傅的Windows内网协议专题,感觉自己Kerberos等协议学的是个屁,于是想来再研究一下,遂有此水文

之前还写过一篇水文,仅有过程分析,未抓包分析,仅供参考:浅谈Windows身份认证

涉及到的攻击方法有:

AS-REP Roasting

枚举域用户

黄金票据

白银票据

Kerberoasting

约束委派

MS14-068

测试环境

测试使用的服务器如下,都属于域centoso.com

角色 操作系统 IP 主机名
域控(DC) Windows Server 2008 R2 192.168.206.100 WIN08
域成员机 Windows 7 Ultimate 192.168.206.104 Loong716-WIN7

测试是在WIN08(域控)上使用wireshark抓包,与此同时在WIN7上使用域用户test进行登录,抓取到的Kerberos数据包如下:

1578578950279.png

Kerberos数据包分析

Kerberos的主要步骤如下,重点掌握前四个过程:

  1. AS-REQ
  2. AS-REP
  3. TGS-REQ
  4. TGS-REP
  5. AP-REQ
  6. AP-REP

有些名词解释可能不太标准,可以看一下微软文档的这一篇:Glossary

AS-REQ

AS_REQ数据包的整体结构是这样的:

1578623369050.png

其中的kdc-optionsetype太长了我单独截出来:

1578623791300.png

1578623810050.png

我们从头开始看一下比较重要的信息

pvno

1578624647469.png

pvno表示的是Kerberos协议的版本,这里为5表示的是使用Kerbeos V5

msg-type

1578624671999.png

顾名思义,消息类型,此处为krb-as-req

padata

1578624521648.png

padata被称做pre-authentication data(我也不知道怎么翻译。。身份预验证数据?),里面存放的是一些认证信息。

padata的类型(type)有很多种,可以参考这篇文章:Kerberos PA-DATA,在AS-REQ过程中padata主要有两部分:

  1. PA-DATA PA-ENC-TIMESTAMP 这是是使用用户hash加密的时间戳,发送到KAS(Kerberos Authentication Service)后KAS使用数据库中对应用户的hash来解密出时间戳,时间戳在指定范围内则认证通过。 如果域内设置”Do not require Kerberos preauthentication”这个选项,那么将不会做预认证这个过程,此时向域控制器的88端口发送AS-REQ请求,域控就会返回相应的AS-REP,对该返回包进行处理就可以拿到用户的hash(注意接下来的AS-REP中的enc-part使用用户hash加密的,且可解密),从而进行爆破,这就是AS-REP Roasting

  2. PA-DATA PA-PAC-REQUEST 1578625464878.png这里是微软自己引入Kerberos的PAC(Privilege Attribute Certificate)是否开启的一个设置,我理解的PAC是一个做权限认证的扩展。MS14-068的漏洞成因就是在PAC上,后面会分析

req-body

接下来是请求的主体部分:主要是以下几个方面:

  1. kdc-options 这个我自己理解的就是一些标志位(类似于TCP的标志位),例如renewable的标志位为1,则表示可以使KDC定期续签使用寿命长的票证。我没找到很全面详细的资料,可以参考一下微软文档Kerberos Ticket Options

  2. cname 这个主要包含的就是请求的用户和所在域的名称,此处我的用户名为test,属于NT-PRINCIPAL,value值为1(daiker师傅在文章里提到了该用户存在/不存在两种情况返回的数据包不同,可以用来枚举域用户)。 用户类型有以下几种(最前面的数字为value值):

    • 0: NT-UNKNOWN Name type not known
    • 1: NT-PRINCIPAL Just the name of the principal as in DCE, or for users
    • 2: NT-SRV-INST Service and other unique instance (krbtgt)
    • 3: NT-SRV-HST Service with host name as instance (telnet, rcommands)
    • 4: NT-SRV-XHST Service with host as remaining components
    • 5: NT-UID Unique ID

PS:枚举域用户时判断用户是否存在时,判断的是另一个标志,可以参考daiker师傅和3gstudent师傅的文章:
AS_REQ & AS_REP #域用户枚举 渗透技巧-通过Kerberos-pre-auth进行用户枚举和口令爆破

  1. sname 这个包含的是服务端的身份,还有所在域名称。till为到期时间,nonce为随机生成数

  2. etype 这个不用说大家也看得出来,是加密类型。KDC通过etype中的加密类型来选择用户对应的hash来解密,因此注意对应关系


AS-REP

AS-REP的数据包的结构为:

1578637655531.png

与前面重复的部分就不细说了,主要来看ticketenc-part这两部分

ticket

因为AS-REP这个过程返回的是用krbtgt账户hash加密的,本质是一张TGT,因此我们如果能够拿到krbtgt的账户hash和SID的话就可以来伪造一张TGT,这就是黄金票据

  1. tkt-vno 票据格式的版本号

  2. realm 派发该票据域的名称

  3. sname 该票据所属的服务端的身份

  4. enc-part 用KDC的密钥(即krbtgt的密钥)加密的票据部分

enc-part

注意这个enc-part有别于ticket中的enc-part,这里的加密使用的是用户的密钥,也就是使用用户hash加密的Logon Session Key,这个属于在下一步认证中用到的会话密钥


TGS-REQ

TGS-REQ数据包主要是两大部分:

tgs-req

1578640576555.png

其中几个主要部分内容如下:

  1. padata -> ap-rep -> ticket 这是之前AS-REP得到的TGT的票据结构,此时用来请求TGS
    • sname:该票据所属的服务端的身份
    • enc-part:用KDC的密钥加密的票据部分,可以看到和AS-REP中返回的ticket -> enc-part是一样的
  2. padata -> ap-rep -> authenticator 这里是经Logon Session Key加密的Authenticator,也是属于在下一步认证使用的会话密钥

req-body

1578640594375.png

这里的基本前面已经说过了。sname是要请求的服务的名称(因为我们的操作是登录Loong716-WIN7.centoso.com这台域成员服务器)


TGS-REP

TGS-REP数据包结构如下:

1578642431782.png

其中主要需要了解的就是ticketenc-part

ticket

这里返回的ticket就是一张ST(Service Ticket),很多时候也被叫做TGS。这张票据的enc-part是使用请求的服务端密钥进行加密的

因此如果我们在渗透过程中拿到了服务账户的hash,就可以使用该hash伪造一张ST/TGS,从而去访问对应的服务,这就是票据传递攻击(Pass The Ticket)中的白银票据

我们同样也可以请求对应服务的ST/TGS,从里面提取出服务账户的hash,然后进行爆破,这就是Kerberoasting攻击

enc-part

这个enc-part是经过Logon Session Key加密的Service Session Key,用于请求服务时的会话密钥


S4U

S4U协议是微软为Kerberos做的扩展协议,主要有S4U2self和S4U2proxy,应用在约束委派

还是先贴一张流程图便于理解:

enter image description here

S4U2self

S4U2self扩展允许Service 1代表特定User向KDC请求Service 1的可转发的票据

有时用户会通过其他协议(例如NTLM或基于表单的身份验证)对服务进行身份验证,因此他们不会将TGS发送给服务。在这种情况下,服务可以调用S4U2Self来要求身份验证服务为其自身的任意用户生成一个TGS,然后可以在调用S4U2Proxy时将其用作“证据”

当发出请求的用户的userAccountControl中设置了TRUSTED_TO_AUTH_FOR_DELEGATION字段时,你就可以对该用户使用S4U2self,并且不需要用户的密码

在数据包中使用PA-FOR-USER数据结构,利用用户名和realm来向KDC标识用户:

1583732345120.png

并且请求的是可转发的TGS:

1583733181342.png

获得可转发的TGS之后就表明User确实已对调用S4U2Proxy的服务(这里是Service 1)进行了身份验证

S4U2proxy

S4U2proxy要求Service 1的服务票据设置了可转发标志(即通过S4U2self获得的服务票据),然后向msds-allowedtodelegateto中指定的服务请求服务票证

在数据包中通过S4U2self获得的服务票据(Service 1的TGS)会被放在additional-tickets中,向KDC请求Service 2的TGS

1583735600670.png

可以看到sname是WIN7的CIFS服务:

1583736279798.png

而且这里也要设置forwarded标志位:

1583736587380.png

PS:如果TGS的服务票不是可转发的话,S4U2proxy仍然可以进行,但生成的TGS是不可转发的:

如果服务1 主体上的TrustedToAuthenticationForDelegation参数设置为:

  • True:KDC必须 在S4U2self服务票证中设置FORWARDABLE 票证标志([RFC4120] 2.6节)。

  • FALSE和ServicesAllowedToSendForwardedTicketsTo 非空:KDC不得在S4U2self服务票证中设置FORWARDABLE票证标志([RFC4120]第2.6节)。

如果DelegationNotAllowed上的主要参数被设置,则KDC不应在S4U2self服务票据设定的转发票证标志([RFC4120],第2.6节)。

But…

当利用的是基于资源的约束委派时,即使提供的TGS不是可转发的,S4U2proxy仍可以正常工作

但当用户被设置为对委派敏感时,S4U2proxy将会失败

“If the service ticket in the additional-tickets field is not set to forwardable<20> and the PA-PAC-OPTIONS [167] ([MS-KILE] section 2.2.10) padata type has the resource-based constrained delegation bit:

  • Not set, then the KDC MUST return KRB-ERR-BADOPTION with STATUS_NO_MATCH.

  • Set and the USER_NOT_DELEGATED bit is set in the UserAccountControl field in the KERB_VALIDATION_INFO structure ([MS-PAC] section 2.5), then the KDC MUST return KRB-ERR-BADOPTION with STATUS_NOT_FOUND.”


PAC分析

在之前的AS-REQ中提到了微软为Kerberos添加的扩展PAC,以及由PAC引起的漏洞MS14-068,这个章节主要对MS14-068的利用过程进行分析,同时了解PAC的特性

PAC

关于PAC,中文应该译为权限属性证书,其定义Microsoft Docs里面是这样写的:

The Privilege Attribute Certificate (PAC) Data Structure is used by authentication protocols (protocols that verify identities) to transport authorization information, which controls access to resources. Once authentication has been accomplished, the next task is to decide if a particular request is authorized. Management of network systems often models broad authorization decisions through groups; for example, all engineers who can access a specific printer or all sales personnel who can access a certain web server. Making group information consistently available to a number of services allows for simpler management.

后面还有几段话,感觉贼抽象,大体意思就是PAC是微软设计的一个扩展,主要用于Kerberos协议上,域控会把PAC放在Kerberos的票据里,当用户使用票据时,服务端可以通过PAC来确定用户是否有权限

PAC内存放有请求用户的User SID、Group SID等,一般这种验证的证书为了防止被篡改肯定会有签名,微软确实给PAC设计了两个签名,分别是服务校验和KDC校验和

服务校验和使用服务端的密码加密,KDC校验和使用KDC的密码加密

最终加密PAC的密钥被放在Authenticator的subkey中,服务端使用该密钥解密出PAC然后再进一步验证

PAC的结构就不多讲了,微软的文档以及daiker师傅的文章写的很清楚,直接结合exp和数据包分析一下MS14-068

MS14-068

按微软对PAC的设计来说,其签名的加密规定的是HMAC系列的checksum算法,我们没有krbtgt的密钥时,肯定是无法伪造PAC的

而在实际情况中,Windows却允许使用任意算法来计算校验和,而且这个加密算法是由客户端来指定的,KDC会根据客户端指定的算法来验证签名。因此我们只要修改PAC的签名加密算法为MD5,就可以任意修改PAC,并且还能通过KDC的认证

Question 1:如何伪造PAC?

首先我们看一下MS14-068利用时的数据包:

1578668833531.png

查看第一个AS-REQ,我们与之前正常的Kerberos认证的数据包对比可以发现这里的include-pac的值为False,这其实是微软允许的一个设置,当该属性设置为False时代表客户端请求一张不包含PAC的TGT

1578668681221.png

我们查看exp也可以看到构造数据包时将include-pac设置为了False,之后成功接收到AS-REP,拿到一张没有PAC的TGT

1578669191516.png

1578669127020.png

之后要做的是伪造一个高权限PAC来添加到之后的TGS_REQ过程中,可以看到Exp在构造TGS-REQ数据包时调用了build_pac()函数

1578669258091.png

跟一下build_pac()这个函数,太长了直接贴代码了:

def build_pac(user_realm, user_name, user_sid, logon_time, server_key=(RSA_MD5, None), kdc_key=(RSA_MD5, None)):
    logon_time = epoch2filetime(logon_time)
    domain_sid, user_id = user_sid.rsplit('-', 1)
    user_id = int(user_id)
    
    elements = []
    elements.append((PAC_LOGON_INFO, _build_pac_logon_info(domain_sid, user_realm, user_id, user_name, logon_time)))
    elements.append((PAC_CLIENT_INFO, _build_pac_client_info(user_name, logon_time)))
    elements.append((PAC_SERVER_CHECKSUM, pack('I', server_key[0]) + chr(0)*16))
    elements.append((PAC_PRIVSVR_CHECKSUM, pack('I', kdc_key[0]) + chr(0)*16))
    
    buf = ''
    # cBuffers
    buf += pack('I', len(elements))
    # Version
    buf += pack('I', 0)
    
    offset = 8 + len(elements) * 16
    for ultype, data in elements:
        # Buffers[i].ulType
        buf += pack('I', ultype)
        # Buffers[i].cbBufferSize
        buf += pack('I', len(data))
        # Buffers[0].Offset
        buf += pack('Q', offset)  
        offset = (offset + len(data) + 7) / 8 * 8

    for ultype, data in elements:
        if ultype == PAC_SERVER_CHECKSUM:
            ch_offset1 = len(buf) + 4
        elif ultype == PAC_PRIVSVR_CHECKSUM:
            ch_offset2 = len(buf) + 4
        buf += data
        buf += chr(0) * ((len(data) + 7) / 8 * 8 - len(data))

    chksum1 = checksum(server_key[0], buf, server_key[1])
    chksum2 = checksum(kdc_key[0], chksum1, kdc_key[1])
    buf = buf[:ch_offset1] + chksum1 + buf[ch_offset1+len(chksum1):ch_offset2] + chksum2 + buf[ch_offset2+len(chksum2):]
    
    return buf

这个函数里需要关注的主要有以下几个点,涉及到MS14-068的核心的原理:

首先可以看到函数对传参进来的user_sid做了分割:

domain_sid, user_id = user_sid.rsplit('-', 1)
user_id = int(user_id)

1578671479067.png

之后还调用了_build_pac_logon_info()函数对domain_sid等参数做了处理:

elements = []
elements.append((PAC_LOGON_INFO, _build_pac_logon_info(domain_sid, user_realm, user_id, user_name, logon_time)))

跟进一下_build_pac_logon_info()这个函数

1578669676540.png

看到其中调用了_build_groups()函数,继续跟进

1578669701070.png

这个过程的作用就是构造用户的Group SID,可以看到有512、513、518、519、520这几个,看一下微软官方对这几个SID对应的组的定义:

SID Display Name
S-1-5-domain-512 Domain Admins
S-1-5-domain-513 Domain Users
S-1-5-root domain-518 Schema Admins
S-1-5-root domain-519 Enterprise Admins
S-1-5-domain-520 Group Policy Creator Owners

可以看到除了Domain Users外又附加了四个高权限的用户组,利用这个来伪造高权限的SID,进而伪造高权限PAC

接下来再来看对两个签名的构造,调用了checksum()函数,同时也要注意前面定义的server_keykdc_key这两个参数,使用的加密算法都是MD5

1578708809732.png

chksum1 = checksum(server_key[0], buf, server_key[1])
chksum2 = checksum(kdc_key[0], chksum1, kdc_key[1])

checksum()函数其实就是对数据进行MD5运算来构造两个签名

1578669434420.png

Question 2:如何将PAC放入TGT?

然后要做的就是把PAC放入TGT中了,但TGT是经过krbgt密钥加密的,我们看一下Exp是怎么做的(其实是没有放入TGT,采用了别的办法)

首先对比一下build_tgs_req()这个函数的定义以及如何调用的:

1578710340030.png

1578710373459.png

从参数命名来看PAC是作为authorization_data传入build_tgs_req()函数的

再看build_tgs_req()函数的内容,有三点需要关注(对应图中①②③):

  1. 将PAC使用subkey加密
  2. 经subkey加密后的PAC被放在TGS-REQ请求的req-body部分的authorization_data中,也就是enc-authorization-data
  3. 密钥subkey被放在TGS-REQ请求的tgs-req部分的authorization中

1578711116058.png

PS:subkey本质是16位的随机数,从代码中可以看出:

1578711385808.png

对应数据包的req-body -> enc-authorization-data就是刚才生成的PAC:

1578713462395.png

综上所述,漏洞利用时并没有将伪造的PAC放入TGT,而是放在TGS_REQ数据包的enc-authorization-data中,但include-pac仍然是False,而这样服务端竟然可以成功解密PAC,不晓得微软当时设计时的想法是什么

Question 3:服务端返回的是TGT还是ST?

服务端对TGS-REQ的处理就不细说了,大体就是服务端会接收客户端发送过来的TGT,并且解密出数据包的PAC,重新使用Server_Key和KDC_Key生成签名,返回一个正常的包含PAC的TGT给客户端(这个返回的是TGT还是ST说法不一,我觉得两种说法都有一定道理,但个人更倾向于返回的是TGT)

原理我也搞不懂。。大型魔幻漏洞

至于为什么会纠结是TGT还是ST/TGS,主要是因为我抓了完整的MS14-068利用过程的数据包后(最后注入票据后我是用的dir \\WIN08\c$来访问域控的C盘),发现前后过程是这样的:

这是MS14-068生成票据这个过程的数据包:

1578726406635.png

这是注入票据后执行dir \\WIN08\c$的数据包:

1578726640906.png

可以看到又重新做了TGS-REQ和TGS-REP这一过程,这我就懵逼了,如果说返回的是ST的话,为啥又要请求一遍TGS?

Example 1:漏洞利用过程票据的变化

于是我想先来看一下漏洞利用过程中内存中票据的变化。

使用kerberos::ptc注入使用Exp生成的TGT_test@centoso.com.ccache后,此时内存里只有一张票据(注入前已经先清空了票据)

1578733499096.png

而执行dir \\WIN08\c$后,内存中共有三张票据

1578733620367.png

可以看到此时内存中已经有了CIFS的ST(CIFS是SMB的另一个名字)

Example 2:MS14-068与缓存证书

MS14-068在2014年刚爆出来的时候,很多人在域用户登录后用Exp复现发现不能成功(使用dir \\WIN08\c$来访问域控C盘时提示没有权限),但用域成员机的本地账户就可以,于是当时有很多人推测只有使用本地账户才能够利用MS14-068(注意:当时很多人注入EXP生成的票据前并没有清空内存中的票据)

分析之前可以先声明一下这个观点是错误的,域内账户也是可以利用MS14-068的

先来科普一个概念,之前在网上找相关资料时看到了深入解读MS14-068漏洞:微软精心策划的后门?这篇文章,在这篇文章中作者提到了缓存证书这一概念:

Credentials Cache

The credentials cache is managed by the Kerberos SSP, which runs in the LSA's security context. Whenever tickets and keys need to be obtained or renewed, the LSA calls the Kerberos SSP to accomplish the task.

From the client's point of view, a TGT is just another ticket. Before the client attempts to connect to any service, the client first checks the user credentials cache for a service ticket to that service. If it does not have one, it checks the cache again for a TGT. If it finds a TGT, the LSA fetches the corresponding logon session key from the cache, uses this key to prepare an authenticator, and sends both the authenticator and the TGT to the KDC, along with a request for a service ticket for the service.

In other words, gaining admission to the KDC is no different from gaining admission to any other service in the domain—it requires a session key, an authenticator, and a ticket (in this case, a TGT).

缓存证书的概念大致就是:当客户端请求一个服务时,会先在客户端寻找相对应的ST;如果没有ST的话,再去寻找TGT;如果有TGT的话就用它去请求相对应的ST

我们看一下域成员刚登录服务器时,内存中都有什么票据:

1578736982783.png

一共5张票据,后面还有一个LDAP的票我就不截图了。可以看到在域账户刚登录上服务器时,内存中就已经有一张CIFS的ST了

还记得上面Example 1中我们访问域控的C盘后,内存中有哪些票据吗?是不是也有一张CIFS的ST,因为我们执行dir \\WIN08\c$命令时走的是SMB/CIFS协议

那再结合一下缓存证书的概念,就能理解当时为什么有人会说域账户利用不了MS14-068了:

  1. 白帽子在注入Exp生成的票据前,没有清空内存中的票据,内存中存在一张CIFS的ST,在下文中记作票据1
  2. 使用mimikatz注入Exp生成的票据后,白帽子使用SMB/CIFS协议请求访问域控C盘
  3. 按照缓存证书的概念,服务器先在本地寻找CIFS的ST,于是找到了票据1,使用票据1去请求服务
  4. 而票据1是域普通成员的票据,并不是注入的高权限PAC后请求得到的票据,因此访问失败

我认为这也可以侧面印证MS14-068返回过来的票据是TGT

总之整个过程是极其的魔幻。。理论上讲TGS-REP只有在跨域的情况下才会返回TGT,其他情况都要返回ST

在微软官方文档找到了一个点,但文档的描述很模糊,甚至还会与之前的结论矛盾,无法实锤:

1578753305961.png

这里说如果一个TGS-REQ的TGT没有包含PAC,在签发Service Ticket之前必须添加一个PAC。但这个PAC是哪个PAC?添加到哪里?如果是添加到TGT那会不会返回给客户端?文档并没有做详细的描述,菜鸡我能力有限,无法验证,暂时也没找到相关的资料


参考文章

https://www.anquanke.com/post/id/190261 https://www.anquanke.com/post/id/190625 https://www.anquanke.com/post/id/192810 https://www.freebuf.com/vuls/56081.html https://www.cnblogs.com/feizianquan/p/11760564.html http://www.eventid.net/articles/article_kerberos_ticket_options.asp https://docs.microsoft.com/zh-cn/archive/blogs/apgceps/packerberos-2 https://cwiki.apache.org/confluence/display/DIRxPMGT/Kerberos+PA-DATA https://labs.f-secure.com/archive/digging-into-ms14-068-exploitation-and-defence/ https://shenaniganslabs.io/2019/01/28/Wagging-the-Dog.html#serendipity https://www.harmj0y.net/blog/redteaming/another-word-on-delegation/ http://www.harmj0y.net/blog/activedirectory/s4u2pwnage/ https://labs.f-secure.com/archive/trust-years-to-earn-seconds-to-break/ https://medium.com/@robert.broeckelmann/kerberos-wireshark-captures-a-windows-login-example-151fabf3375a https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-pac/c38cc307-f3e6-4ed4-8c81-dc550d96223c