域内提权票据篇之剖析经典漏洞MS14-068

域内提权票据篇之剖析经典漏洞MS14-068 一、背景知识 1.PAC介绍 Kerberos 协议最初设计的几个流程里说明了如何证明 Client 是 Client 而不是由其他人来冒充的,但并没有声明 Client 有没有访问...

域内提权票据篇之剖析经典漏洞MS14-068

一、背景知识

1.PAC介绍
Kerberos 协议最初设计的几个流程里说明了如何证明 Client 是 Client 而不是由其他人来冒充的,但并没有声明 Client 有没有访问某个 Server 服务的权限, 因为在域中不同权限的用户可以访问的资源是有区别的,所以微软为了解决这个问题在 Kerberos 认证时加入了 PAC (Privilege Attribute Certificate,特权属性证书),可以理解为飞机上的座位有头等舱和经济舱,通过区分带有PAC的TGT(Ticket Granting Ticket)来决定你属于哪个座位。

1.1 PAC的实现
当用户与 KDC(Key Distribution Center,密钥分发中心) 之间完成了认证过程之后, Client需要访问Server所提供的某项服务时,Server为了判断用户是否具有合法的权限需要将Client的 User SID 等信息传递给KDC, KDC通过 SID判断用户的用户组信息、用户权限等, 进而将结果返回给Server, Server再将此信息与用户所索取的资源的 ACL进行比较, 最后决定是否给用户提供相应的服务。
PAC 会在 KRB_AS_REP 阶段由 AS(Authentication Server,认证服务器) 放在 TGT 里加密发送给 Client, 然后由 Client 转发给 TGS(Ticket Granting Server,票据授予服务器) 来验证Client所请求的服务。
在PAC中包含有两个数字签名PAC_SERVER_CHECKSUM (在PAC数据用于访问控制之前,必须检查该签名,用于验证PAC数据的提供者是否知道服务器的密钥) 和PAC_PRIVSVR_CHECKSUM (用于验证PAC是否是由KDC颁发), 这两个数字签名分别由 Server 端密码 HASH 和 KDC 的密码 HASH 加密;同时 TGS 解密之后验证签名是否正确, 然后再重新构造新的 PAC 放在 ST(Service Ticket,服务票据) 里返回给客户端, 客户端再拿着 ST 发送给服务端进行验证。

1.2 Server与PAC
PAC 可以理解为一串校验信息,为了防止被伪造和串改,原则上是存放在 TGT 里,并且 TGT 由 KDC hash 加密。 同时尾部会有两个数字签名, 分别由 KDC 密码和 server 密码加密,防止数字签名内容被篡改。

同时 PAC 指定了固定的 User SID 和 Groups ID,还有其他一些时间等信息, Server 的程序收到 ST 之后解密得到 PAC 会将 PAC 的数字签名发送给 KDC, KDC 再进行校验然后将结果已 RPC 返回码的形式返回给 Server。

二、前言

该漏洞允许攻击者将域内任意普通用户权限的账户,提权到域管理员的权限,多数产生在 Windows server 2008 和 Windows server 2003 的域环境中。

接下来通过对Pykek源码的分析来剖析MS14-068漏洞原理。

三、原理分析

1、AS-REQ

Client向KDC(AS)发起申请TGT请求(AS-REQ)阶段,这里攻击构造为向域控制器请求一张不带PAC的TGT票据(这个操作在微软是被允许的),这是微软默认的设计:

  1. KERB-PA-PAC-REQUEST ::= SEQUENCE {
  2. include-pac[0] BOOLEAN -- if TRUE, and no pac present,
  3. -- include PAC.
  4. ---If FALSE, and pac
  5. -- PAC present, remove PAC
  6. }
  • include-pac:该字段指示是否包含 PAC。如果该值为 TRUE,则包含 PAC,独立于其他预身份验证数据。如果该值为 FALSE,则即使存在其他预身份验证数据,也不会包含 PAC。

攻击代码,在这个请求的构造过程中,通过设置 pac_request=False 来实现:

as_req = build_as_req(user_realm, user_name, user_key, current_time, nonce, pac_request=False)

通过PCAP包可以更直观的看到在 AS-REQ 请求中的 include-pac:False 字段,这是造成漏洞的第一个因素:

接着构造请求的body部分,可以看到这里将通信加密算法指定为RC4_HMAC:

  1. req_body = build_req_body(target_realm, 'krbtgt', target_realm, nonce, cname=user_name)
  2. def build_req_body(realm, service, host, nonce, cname=None, authorization_data=None, etype=RC4_HMAC)

然后通过user_key(即已知的普通域用户密码NLTM哈希值)加密时间戳,以便域控能在其认证库中通过匹配user的密码成功解密,即为了通过身份认证的过程。

构造完成后,发送数据:

sock = send_req(as_req, kdc_a)


由于是用域内普通用户的账号密码申请,所以是可以被验证通过的。

2、AS-REP

当KDC(AS)收到请求后会进行响应(AS-REP)阶段,并会发回一个由用户密码加密的session_key和不带PAC的TGT(抓包可以看到这个以07efc开头的TGT票据):


当收到AS-REP后,首先进行解析:

as_rep, as_rep_enc = decrypt_as_rep(data, user_key)

用域内普通用户的账号密码解密KDC返回的请求,解密后获取session_key,用于下一次的通信:

session_key = (int(as_rep_enc’key’), str(as_rep_enc’key’))

即对流量包的这个内容进行解密:

解密获取到session_key和ticket部分(该部分被KDC进行加密,作为域内普通用户是无法解密的):

接下来就是最重要的部分。

3、TGS-REQ

Client拿着AS下发的TGT向TGS申请服务票据(TGS-REQ)阶段,也就是该漏洞的提权阶段。

接下来就会利用攻击脚本进行伪造高权限的PAC,并将其打包在加密的授权数据中,以便获取高权限的票据。由于之前发送的AS-REQ是与域控通信而获取TGT,因此是不含PAC的有效凭据,同时该ticket也是无法解密,因此要解决的问题就是构造高权限的PAC,并将其放在ticket中,还需要让KDC进行正常解析以便获取高权限的票据,攻击脚本使用了两个关键函数来实现 build_pacbuild_tgs_req

构造高权限PAC,需要解决的几个问题:1、尾部签名需要KDC和Server端的hash但我们又没有如何生成有效的签名并让KDC检验通过?2、如何构造高权限?3、原本的PAC是放在TGT里的,如何将PAC放在票据里传输给KDC?

代码通过下面代码实现:

  1. pac = (AD_WIN2K_PAC, build_pac(user_realm, user_name, user_sid, logon_time))
  2. AD_WIN2K_PAC = 128
  3. build_pac(user_realm, user_name, user_sid, logon_time, server_key=(RSA_MD5, None), kdc_key=(RSA_MD5, None)):

签名原本的设计是要用到HMAC系列的checksum算法,也就是必须要有key的参与,但问题就在实现的时候允许所有的checksum算法都可以,包括MD5,所以这里构造PAC中的尾部签名PAC_SERVER_CHECKSUM和PAC_PRIVSVR_CHECKSUM代码如下:

  1. def checksum(cksumtype, data, key=None):
  2. if cksumtype == RSA_MD5:
  3. return MD5.new(data).digest()
  4. elif cksumtype == HMAC_MD5:
  5. return HMAC.new(key, data).digest()
  6. else:
  7. raise NotImplementedError('Only MD5 supported!')

我们可以发现两个签名加密的方式server_key[0]和kdc_key[0]都被程序指定为RSA_MD5,Key的值为None,但原则上来说这个加密方式是应该由KDC来确定,在这里却直接不需要key的MD5构造的签名,只需要将用户构造的data进行MD5运算后,得到的值作为签名,然后直接使用RSA_MD5方式加密,添加检验码,组合成新的PAC,这是漏洞形成的第二个因素:

  1. buf = buf[:ch_offset1] + chksum1 + buf[ch_offset1+len(chksum1):ch_offset2] + chksum2 + buf[ch_offset2+len(chksum2):]

同时在这个过程中还有user_sid这个参数,build_pac函数会将其分割,为了重新构造高权限的sid值:

如用户的sid为 S-1-5-21-1872928177-2865231364-3848160764-1107,最后一个 - 链接的数字则代表了不同权限的用户组。其中512、520、518、519分别为不同的组的sid号,通过这种方式构造了包含高权限组SID的PAC。

然后将构造的高权限PAC添加到数据包中,构造成TGS-REQ请求

  1. 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)

调用函数build_tgs_req,而这里的 authorization_data 即构造生成的pac:

  1. 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):

当PAC不为空的时候,执行下面代码:

这里的AD_IF_RELEVANT 为1,即设置伪造的 TGT 的 AD_IF_RELEVANT 标志位,将伪造的授权数据视为相关数据,从而成功获取服务票据;伪造的PAC需要一个密钥对其加密,这里使用subkey进行加密,subkey是函数 generate_subkey 生成的一串16位的随机数:

  1. subkey = generate_subkey()
  2. def generate_subkey(etype=RC4_HMAC):
  3. if etype != RC4_HMAC:
  4. raise NotImplementedError('Only RC4-HMAC supported!')
  5. key = random_bytes(16)
  6. return (etype, key)

enc_ad即为我们构造的PAC,将其添加到请求中,构造请求体:

  1. req_body = build_req_body(target_realm, target_service, target_host, nonce, authorization_data=enc_ad)
  2. def build_req_body(realm, service, host, nonce, cname=None, authorization_data=None, etype=RC4_HMAC):

通过构造的req_body,可以发现伪造的PAC被加密成密文放在enc-authorization-data里面:

构造好之后需要添加一个检验码:

  1. chksum = (RSA_MD5, checksum(RSA_MD5, encode(req_body))) #key=None

构造完成后PyKek为了使得KDC能够解密req_body中的PAC信息,也需要subkey,因此PyKek将subkey也发送给KDC:

其中的TGT为之前获取的空票据,这里主要通过让KDC进行认证,并将我们的密码通过authenticator传入进去,即:

到现在构造的数据包大体结构为:

之后KDC处理的大体流程就是KDC拿到ap_req之后,会提取authenticator里面的密文,用sessionkey解密获得subkey,再使用subkey解密enc-authorization-data获得PAC,而PAC是我们自己伪造的。

4、TGS-REP

TGS成功响应对伪造的PAC验证之后,会重新进行正常加密流程并返回给Client端一有新的TGT(因为请求的服务是krbtgt,所以返回的TGS票据是可以当做TGT),正常情况下返回的其实是一张用于发送给Server端做认证的ST票据,如果使用之后的服务的话,会换取ST票据。

TGS响应包括使用MD5的新TGT,也就是说这时Client已经获得了一张包含有高权限PAC的TGT票据(40c58开头),后面就可以用这个票据访问任何服务:

四、攻击过程

1、复现过程

工具会将获取的TGT保存生成为ccache缓存文件(该文件就是用于存储用户的票据,以便在后续的会话中可以快速重用这些凭据,避免频繁进行身份验证),此时我们再利用mimikatz将其导入即可拥有高权限,下图是普通用户请求访问域控共享文件夹被拒绝访问:

当我们导入获取的高权限票据后,便可成功访问:

image-20230609115350625

  1. kerberos::purge 清除当前用户(或指定用户)的 Kerberos 令牌缓存
  2. kerberos::list 列出当前用户(或指定用户)的 Kerberos 令牌缓存
  3. kerberos::ptc 导入生成的票据

成功访问:

image-20230609115434219

后渗透阶段可以使用PSTools目录下的PsExec.exe获取shell:

PsExec.exe \\域控主机名 cmd.exe

image-20230609140810860

目前域控的权限已经拿到。

2、漏洞成因

  • include-PAC

通过对include-PAC标志字段的设置,可以得到一个由KDC密钥加密的合法ticket(不含有PAC)。

  • PAC尾部校验码的限制上

通过了解PAC的结构,可以通过修改sid控制权限,因此可以构造高权限的PAC;PAC的尾部签名在Kerberos原本的流程是用server端密码和KDC的密码进行签名,但微软在实现上,却允许任意签名算法,所以客户端可以指定任意签名算法,KDC 就会使用客户端指定的算法进行签名验证。

  • 构造的PAC不在原ticket中

原本PAC是在ticket中,且被KDC密钥进行加密处理,用户是无法进行解密的,但KDC在实现上是允许构造PAC在其他位置的ticket的,并能够正确解析放在其它地方的 PAC 信息。

以PyKek构造的TGS-REQ为例,KDC首先会将TGT(不含PAC)进行解密得到SessionKey-as,其次会从Authenticator中取出来subkey,然后将PAC信息解密,同时会利用客户端设定的签名算法验证签名,在验证成功后,重新采用Server_key和KDC_key生成一个带Key的签名,将其添加到PAC(构造的高权限PAC)信息的尾部,进而重新组合成了一个新的TGT返回给用户(这里其实就是TGS-REP返回了一个TGT信息,而不是ST)。

image-20230609115434219

五、防御&检测

防御:安装KB3011780补丁

检测:根据流量日志特征检测以及主机日志的研判

  • 流量:可以根据上文中提到的标记include-pac为False的特征来初步的筛选;
  • 主机:Windows 安全日志:检查目标服务器上的 Windows 安全事件日志,特别关注 Kerberos 相关事件,异常的 Kerberos 事件可能暗示着漏洞利用尝试,如事件ID:4768(TGT请求)、事件ID:4769(TGS请求)等,后续还有如攻击者使用伪造的票据登录到DC会生成事件ID:4624(账户成功登录),或只有域管有权访问的目标域控制器的共享对象被其他用户访问会生成事件ID:5140(访问网络共享对象)都可以用来研判分析。

六、参考文章

https://adsecurity.org/?p=763

https://learn.microsoft.com/en-us/previous-versions/aa302203(v=msdn.10)

https://github.com/SecWiki/windows-kernel-exploits/tree/master/MS14-068/pykek

  • 发表于 2023-07-26 09:00:00
  • 阅读 ( 7288 )
  • 分类:内网渗透

0 条评论

中铁13层打工人
中铁13层打工人

79 篇文章

站长统计