MS14-068 mimikatz

在上篇文章(传送门)中我已经写了Kerberos的认证流程,如果仔细看过的话我相信你应该已经建立起了对Kerberos认证正常流程的认识。这篇文章主要是为了更深一步挖掘Kerberos协议的细节,以及简单讲讲MS14-068这个漏洞的原理。

Kerberos协议细节

之前在写Kerberos流程的时候我只是讲了身份认证,对于权限认证的事情没有说那么多,只是简单地一笔带过

一点细节

这次在研究MS14-068的时候涉及到了这方面,也就拿出来单独讲讲。

PAC

背景

PAC(Privilege Attribute Certificate),翻译过来就是特权属性证书。PAC是微软在Windows平台上对Kerberos协议的扩充,其设计目的就是为了解决服务器对用户的权限认证问题。因为Kerberos只是解决了身份认证,并没有实现对于访问权限的认证。

也就是说Kerberos只是让服务器确定了访问用户的身份,但是不能让服务器知道用户是否有访问这个服务的对应权限。这对于域的管理是很不利的。而如果要求服务器在验证完Client的身份之后还要向KDC发出请求来确认Client的权限,那对于KDC又是一个负担。

因此微软在Kerberos中又引入了PAC来解决这个问题。

类似于Linux对于用户权限的管理,在Windows域中,每一个用户也拥有自己的SID和所在组的GID,正是通过SID和GID才能使得Server确定Client的权限。

但是Kerberos中有一个背景就是假定网络上传送的数据包可以被任意地读取、修改和插入数据。所以说在Client,Server,KDC这三者中,Server只信任KDC的信息,而且KDC中也的确保存着Client和Server的权限。

PS:(因为KDC在Kerberos协议中就扮演着可信第三方的身份,如果连KDC都不可信,那域信任就无从谈起)

所以说,现在的问题就是,KDC如何把Client的权限信息安全的发送给Server。毕竟只有验证权限之后Server才能给Client提供服务。

结构

PAC

PAC有着自己严密的数据结构,但是正如这篇文章的标题:非硬核向。所以我只讲和MS14-068相关的一部分(毕竟你让我讲别的我也说不出来)。对内部细节感兴趣的可以看我列在参考文章中的博客。

类型意义
0x00000001登录信息。PAC结构必须包含一个这种类型的缓冲区。其他登录信息缓冲区必须被忽略。
0x0000000A客户名称和票证信息。PAC结构必须包含一个这种类型的缓冲区。附加的客户和票据信息缓冲区必须被忽略。
0x00000006服务器校验和。PAC结构必须包含一个这种类型的缓冲区。其他登录服务器校验和缓冲区必须被忽略。
0x00000007KDC校验和。PAC结构必须包含一个这种类型的缓冲区。附加的KDC校验和缓冲区必须被忽略。

我不知道上面的表格内容你们看明白没,反正我是有点懵逼

0x00000001 KERB_VALIDATION_INFO

validation n.生效;批准;验证;确认;证实;核实

这一部分是登录信息,也是PAC最重要的部分,如果被修改的话就会出现大问题。(虽然一般情况下不可能,但是有了MS14-068)

KERB_VALIDATION_INFO是一个条目巨多的结构体,但是最关键的地方就在SID和GID。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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;
0x0000000A PAC_CLIENT_INFO

ClientId(8个字节)

  • 包含在Kerberos初始TGT的authtime

NameLength(2字节)

  • 用于指定Name 字段的长度(以字节为单位)。

Name

  • 包含客户帐户名的16位Unicode字符数组,格式为低端字节序。

官方Doc文档

官方提供的Docs文档居然没有中文,怪不得中文互联网上讲Kerberos内部细节的文章那么少。

0x00000006 Server Signature

由KDC用提供服务的账户(也就是server hash或者是krbtgt hash,取决于REQ包中Sname字段)进行签名,数字签名,防止PAC内容被篡改。

0x00000007 KDC Signatures

由KDC用krbtgt hash签名,数字签名,防止PAC内容被篡改。是以krbtgt hash作为key的签名,以防止不受信任的服务伪造带有无效PAC的票证。

上文提到“以***作为key”是因为在密码学中,生成摘要有两种方式:带key(即键控哈希)和不带key的。我们最熟悉的不带Key的hash算法就是MD5算法,相同的输入只会输出相同的信息摘要。但是在要求安全性比较高的地方就不能使用。例如在Kerberos中,PAC的两个数字签名都要求使用HMAC系列的带Key的校验和(sumcheck)算法,极大的提高了伪造PAC的难度。而在Kerberos中生成签名使用的Key就是Server hash和krbtgt hash,也就是说使用HMAC系列算法来进行消息认证既可以完成信息完整性认证,还能完成信源身份认证。

关于HMAC:https://baike.baidu.com/item/hmac/7307543?fr=aladdin

最后再放一张国外大佬做的图(walkerfuz也是搬运的他的),对应着上面讲的各个结构应该能看明白(一定要看),可以很明确的看出正常情况下TGT和PAC的包含关系。

来自walkerfuz

看不太明白的还有我做的简化版本

简化版本

PAC认证流程

流程图

如果你看过上一篇文章的话,这个流程图你应该不会陌生。这个图和上一篇中相比,修改的地方就是TGT和Ticket包含的内容,在这张图这里我们加入了PAC。

把图翻译成中文就是,在微软版Kerberos实现中,KDC将PAC放在TGT中,TGT加密后从KDC_AS服务经Client-A中转给TGS服务,再放在由TGS服务返回的Ticket中,加密Ticket后经Client-A中转给Server-B

不过要注意的是,TGT和Ticket中的PAC其实是不同的。

KRB_AS_REP中PAC尾部的两个签名都是用krbtgt hash生成的,但是这两个签名内容以及签名的加密算法都是不一样的。因为在身份验证这个过程中,充当Client和KDC_TGS服务的可信第三方是KDC_AS服务,也就是说要请求的服务是KDC_TGS,所以server signature要用krbtgt hash生成。这点从上面的TGT结构图也能看出来。

而在KRB_TGS_REP里包含在Ticket中的PAC,则是在TGT中的PAC被KDC_TGS服务接受之后,KDC在验证过Client A的身份合法后会将PAC解密出来,然后验证尾部两个签名是否合法。如果验证通过KDC就认为PAC没有被篡改,然后将尾部的Server Signature更换为以Server hash作为key生成的签名。

而KDC Signature仍然使用krbtgt hash加密生成的签名(这一点我认为是walkerfuz在他的文章中写错了,他认为用的是Server_Sessionkey作为key进行的签名)

我认为是错误的地方

因为虽然最后PAC被发送给了Server 但是Server并没有解析这个PAC,而是在验证了Server signature之后又把这个PAC发回了KDC,由KDC进行解析再将结果返回给Server。也就是说,虽然PAC被传递给了Client 和 Server,但是Client没有查看和修改PAC的能力,而Server虽然能查看PAC,但是它仍然不能修改PAC。

这一点在微软的官方文档中也写了出来:

文档

微软Docs

飞得更高(指炮塔)

如果你懒得自己看英文的话你就注意看标签2的最开始部分:服务器会传递PAC给操作系统以得到一个Access token。然后服务器的操作系统又把PAC放在KERB_VERIFY_PAC请求中的AP-REQ(这不是KRB_AP_REQ,而是一个数据包中的字段)部分发送给域控(也就是KDC)。

经过“松神”师傅师傅的讨论再引出一个细节。

实际上在AP-REQ中的PAC信息是“不完整的”,上图标签2后半句向我们说明了,在Server向操作系统传递了PAC之后,操作系统把PAC的签名(也就是两个校验和)放进了KERB_VERIFY_PAC中发送给了KDC。

KERB_VERIFY_PAC结构体

从图中我们可以看到,其实这里面不存在PAC,只有两个校验和。和松神讨论认为是为了降低KDC的压力,同时避免造成浪费。(德国佬你学学人家)

Server在收到KDC返回的RPC状态码确认了Client之后才会向Client发送KRB_AP_REP,然后服务才会开始。

不过这也是我根据理论分析的结果,并没有自己实际抓包测试(因为平台有问题wireshark读取不到网络接口)。

MS14-068

可算写到这个了,到这里都已经2200字了,Kerberos就™复杂的离谱(实际上把握住核心思想就不复杂)。

注,MS14-068这一部分我是主要基于walkerfuz大佬的文章进行分析的,他的分析过程的确很精彩。你也可以认为我这部分是辅助理解大佬文章的文章。我在这里也只是写写我对那篇文章的理解(怎么可能)。

原文链接:https://www.freebuf.com/vuls/56081.html

这个漏洞的出现原因是因为KDC采用了弱校验和算法,导致客户端有机会伪造高权限PAC加入TGT中。但我们回顾之前的流程,似乎也没有发现客户端有机会伪造PAC,我们来从流程慢慢分析来解决这个问题。

暴论

在这里我直接提出一个暴论:MS14-068漏洞最核心的步骤是KRB_TGS_REQ。

暴论先放这里,我们先来了解一下Pykek这个知名MS14-068漏洞利用方法的攻击流程。

图解Pykek流程

总流程图

可以看到和之前的图比起来很明显不对称,而且多了很多东西。

不过别担心,我们慢慢来分析这整个流程,本来也就不是几分钟内就能搞懂的东西。

看这个图你会发现我标出了不同颜色的线条。以下是解释

1
2
3
橘色线条=危险
红色线条=高危
绿色线条=大致正常

那么让我们开始吧,不过正常的Kerberos我就不再讲解了,主要是说和MS14-068相关的部分。

恶意KRB_AS_REQ

恶意KRB_AS_REQ

你可以看到我把一个数据项标红了:include_pac:false

微软在Kerberos中加入了PAC之后,为了照顾一些域环境的特殊情况,微软的开发者又加入了一个选项:include_pac,通过控制这个选项我们可以指定这次身份验证是否需要PAC的参与,而MS14-068正是利用了这一点,整个攻击链就是从这里开始的。

下面是正常的KRB_AS_REQ

正常的KRB_AS_REQ

通常情况下,KRB_AS_REQ请求中如果该标志被设置为true,那么只要接下来KDC_AS服务通过对Client A的身份认证,那么就会在返回的KRB_AS_REP数据包中的TGT里面加入PAC信息;对应的,如果被设置为false,那么TGT就不会包含PAC。这也是为了避免伪造PAC后数据包中有多个PAC导致失败。

一般来说,包含PAC信息的KRB_AS_REP数据包都会很大。

KRB_AS_REP

没有PAC的KRB_AS_REP

这一步没什么好说的,就是正常的KRB_AS_REP,而且KDC的确按照KRB_AS_REQ中的设置没有在TGT中添加PAC。

现在Client A就有了一个正常的不包含PAC的TGT了,可以用于之后的请求。

恶意KRB_TGS_REQ(pykek方法)

恶意KRB_TGS_REQ

这里就是MS14-068利用最关键的一步了。所以我们深入一点讲。

在讲这个恶意KRB_TGS_REQ之前,我们先来讲一下正常情况下的请求的结构是什么样子。不过这个图是我简化的版本,而且因为简化有些地方结构不太准确(讲内部结构的文章太少了,很多时候只能参考官网英文文档),只保留了和MS14-068相关的部分,实际上的请求包内容复杂得多,是一组很严密的结构。

正常的KRB_TGS_REQ

mgs-type

很简单,一个标识符,这里的内容表明了这是个什么请求。

PA-DATA

这里包含两个重要部分:include_pac标志和AP-REQ

但是要注意,这个AP-REQ可不是KRB_AP_REQ,而是发送给KDC包含着Client A所请求的Server B服务的信息。

AP-REQ

这里就是存放TGT的地方了,而TGT是用krbtgt hash加密的,所以Client无法查看PAC内容。而且就算拿到了krbtgt hash也不能修改PAC,因为有着两个校验和的存在。

但是话又说回来,既然都有了krbtgt hash那我们为什么不干脆伪造黄金票据呢(笑。

REQ-BODY
Sname

这个是要请求的服务,TGS_REP获得的ticket是用该服务用户的hash进行加密的。

有个比较有意思的特性是,如果指定的服务是krbtgt,那么拿到的TGS票据(Ticket)是可以当做TGT用的。因为TGT只是一种特殊类型的Ticket,两者在数据结构上没有什么太大的不同。

enc-authorization-data(可选项)

内部是用密钥加密的信息,一般是用子会话密钥(sub-sessionkey)加密的,而sub-sessionkey存在于PA-DATA字段下AP-REQ中的Authenticator字段。也可以直接用SessionKey加密。这个信息只会存在于KRB_TGS_REQ请求中。

不知道我理解的对不对,原文来自于RFC1510(当然RFC4120里面也有),巨离谱,是一份1993年的112页全英文文档,我人都傻了

关于sub-SessionKey的解释我在RFC4120文档中找到了

Sub-session key
A temporary encryption key used between two principals, selected and exchanged by the principals using the session key, and with a lifetime limited to the duration of a single association. The sub-session key is also referred to as the subkey.
翻译下来大致是这样:
子会话密钥
在两个主体之间使用的临时加密密钥,由主体使用会话密钥进行选择和交换,并且其生存期仅限于单个关联的持续时间。子会话密钥也称为子密钥。

sub-session key可以由Client指定。

想了解更多建议查看文档原文

RFC4120:https://datatracker.ietf.org/doc/rfc4120/?include_text=1

RFC1510:https://tools.ietf.org/html/rfc1510

在了解完正常的KRB_TGS_REQ之后我们就可以研究Pykek到底是怎么构建一个恶意请求的了。

恶意请求

首先,我们来确定一下我们手头有什么材料:

  • 一个没有PAC信息的正常TGT
  • 可以伪造的高权限PAC。

还有KDC的特性:

  • 可以操作的include-pac标识;
  • 可以用于单次会话可由Client发送的临时加密密钥subkey;
  • 可以用subkey将信息加密放在enc-authorization-data中
  • 如果指定的服务是krbtgt,那么拿到的TGS票据(Ticket)是可以当做TGT用的。

似乎我们已经有了进行攻击所需的全部资源。

但是,这些还不够,我们还面临几个问题:

  1. 虽然我们有正常TGT,但是我们没有krbtgt hash,无法将伪造的高权限PAC放进去。
  2. include-pac标识虽然可以操控,但是似乎也没什么可利用的点。
  3. Kerberos要求PAC必须有服务器校验和以及KDC校验和,而且都需要有Key参与,也就是从理论上来说哪怕我们能伪造PAC权限信息也不能伪造签名,那™不是白忙活。

这波咋办嘞?分析了半天什么操作都打出来了,难道要在最后一波团灭?

幸好,出于一些未知的原因,对手开始犯蠢主动配合我们操作了。

对于前两个问题,如果我们将PAC用自己生成的subkey加密放在KRB_TGS_REQ中的enc-authorization-data字段,然后再用SessionKey将subkey加密放在authenticator中。那么KDC就会配合我们,用SessionKey解密出subkey再解密出我们生成的高权限PAC信息并且成功解析。

对于第三个最要命的问题,KDC的行为更离谱,Kerberos要求两个签名都必须有Key参与,但是微软的实现却使得可以使用任意算法来进行签名,而且算法也是由Client指定的。

WTF???

也就是说我们可以用MD5算法来对PAC进行签名,只要我们不改变PAC的信息,那么签名就肯定会通过KDC的验证,毕竟这里的签名已经失去了对信源身份认证的效果。而这点也就是MS14-068漏洞的核心:“微软Kerberos实现支持弱哈希机制,导致其可以用于伪造信息”。微软的修复补丁也是添加了对签名算法类型的校验。

实际上在我搜到的国外博客中,有一篇提到“MS14-068其实是MS11-013在不同代码区域中的重复,…………,而且鉴于在补丁发布之前,漏洞已经被积极利用,所以其实MS14-068可能已经被高级攻击者使用了很长一段时间了。“也就是说MS14-068哪怕是在14年,也是被人玩剩下的东西了,在漏洞挖掘这条路上我们还有很长距离要走。

经过上面两波迷惑操作之后,攻击者就成功的把伪造的高权限PAC和TGT给发送给KDC_TGS服务了,而且KDC在检查过之后发现“并没有什么问题”,然后就开始准备回复了。

但似乎还有一个不太对的地方,不过我们放到下一部分来说。

畸形KRB_TGS_REP

我们之前在介绍PAC的时候讲过,KDC在验证完Server signature和KDC signature之后会在更换这两个签名。对于Server signature,KDC用接收到的REQ数据包中Sname字段指定的服务的账户hash进行签名;而KDC signature就还是krbtgt hash。

而我们发送的恶意TGS_REQ请求中,Sname字段指定的服务却是krbtgt(和AS_REQ指定的服务一致),也就是说KDC这次会用krbtgt hash进行Server signature的签名。(有没有感到似乎抓到了什么头绪?)

我们先把PAC暂时放一放,看看别的东西。

还记得正常的KRB_TGS_REP会发送什么给Client吗?

KDC会发送Ticket和{Server_SessionKey}SessionKey。

但是,我们在发送恶意TGS_REQ请求时,虽然信息标识为TGS_REQ,但是服务请求对象(Sname)却是krbtgt,也就是说之前的TGS_REQ请求其实可以看做是套着TGS_REQ皮的AS_REQ请求。

而KDC看到Sname为krbtgt后就会用krbtgt hash来加密“Ticket”(是不是又察觉了什么?)。而后,KDC又用subkey加密了SessionKey,最后和“Ticket”一起发送给了Client。但这个回复其实是个”缝合怪“。我们用公式来表示一下这个回复。

畸形KRB_TGS_REP=“Ticket”,{SessionKey}subkey

"Ticket"={SessionKey,Client A信息,正确签名的高权限PAC}krbtgt hash

看起来和AS_REP真的是一毛一样(区别肯定还是有的)!

正常的AS_REP

也就是说,在KDC在TGS_REP中回复给了Client一个TGT,而不是一个Ticket,虽然两者的相似之处可能有99%,但就是那1%的不同决定了它们的真实属性(听起来有点像人与猿)。

现在,攻击者就拿到了实施攻击的所有资源:

  • 一个包含着高权限PAC的TGT
  • 用于和KDC通讯的SessionKey

那接下来就是整活儿时间了。

整活儿

其实接下来的发生的事情就和一个高权限用户的正常访问近似。还是老样子,KRB_TGS_REQ-》KRB_TGS_REP-》KRB_AS_REQ-》KRB_AS_REP。然后服务开始,攻击者该干啥干啥,需要访问下一个服务的话就再走一遍攻击流程,获得下一张Ticket。后续的更具体的攻击手法也就不在这篇文章的讨论范围了。

关于我为什么不认为MS14-068是微软的阴谋

可能国内现在的信息安全学习者对于MS14-068这个漏洞的内部原理理解大都来自于walkerfuz在freebuf上发表的文章,文章的标题就是《深入解读MS14-068漏洞:微软精心策划的后门?》,老实说,我一开始学习的时候也被这个阴谋论唬住了,跟着他的文章一步一步分析下去,似乎也就是有些阴谋论的样子,毕竟这个漏洞的确有很多“巧合”。

但是随着我一步一步挖掘细节,去参考官方文档和国外的一些安全博客进行学习,我发现这个阴谋论似乎逐渐被推翻了。

因为我发现,walkerfuz在他文章中列举的三个“巧妙”的错误,其实内部都是有合理的逻辑的。

三个“巧妙”的错误

我就按照他说的错误来一步一步证明我的观点:

  1. 关于可以由Client指定签名算法,实际上来说,Client根本就没有指定签名算法。因为从根本上来讲,正常情况下PAC都是由KDC生成的,何来Client指定签名算法一说。KDC从来不相信别的主体,它只相信自己以及只能由自己发送的数据。MS14-068中,所谓攻击者指定了PAC签名算法,实际上只是KDC相信了这个伪造的PAC是自己发送的,况且,本身PAC的设计规范就是要求必须要声明签名算法类型的。
  2. 关于KDC可以解析放在请求包中其它地方的PAC。对于这点,我猜测可能是微软在数据包解析时没有检查PAC数据的来源,而是在数据包中解析到了PAC之后就验证PAC的签名,哪怕这个PAC并没有在TGT中。而微软这么实现的理由可能是因为过于信任PAC的签名机制,毕竟正常情况下就算伪造的高权限PAC被解析,也会因为签名校验无法通过而导致攻击失败;当然也可能只是单纯因为KDC在接收到包文之后直接各项提取出来然后分发给各个信息处理单元该干嘛干嘛,程序这样实现也是很正常的。而“放在其它地方”,这个其它地方也是很有讲究的,利用的也是Kerberos的正常机制,Kerberos允许在一个请求会话中包含一个子会话,而子会话的内容就由子会话密钥(subkey)加密之后放在REQ-BODY中的enc-authorization-data字段,MS14-068正是利用了这一点。
  3. KDC从authenticator中提取出subkey来解密出PAC也是很正常的,符合子会话要求。至于重新采用自身的krbtgt hash分别作为key生成Server signature和KDC signature是因为在恶意KRB_TGS_REQ请求中,指定了Sname字段内容为krbtgt。至于再用subkey加密SessionKey,同新生成的TGT一起发送回Client,应该和Kerberos内部关于子会话的机制有关,但是这个信息太少,也只是我的猜测。

总结一下,MS14-068正是攻击者利用微软对于PAC签名实现允许弱哈希校验这个错误,配合上Kerberos自身的一些称不上漏洞的机制,组合而成的一个漏洞。是特意设计的“后门”的概率不大,如果它是阴谋,我们也可以思考一下,这个漏洞会不会对西方的企业产生危害,很明显,它会,所以阴谋论根本上就不太站得住脚。不过,就算它不是后门,我们仍然要警惕,在一些涉及到国家安全的领域,一定要用我们自己的东西,哪怕它也存在一些漏洞,也比直接被用预留后门轻轻松松攻破要好。

参考文章

总体学习:

KRB请求细节:

MS14-068:

密码学部分:

官方文档:

鸣谢:

松神

后记

写这篇文章真的花了巨久,前前后后自闭,查博客,查官方文档,复习了一遍密码学基本概念,大半个月才结束,写了接近6000字,加上之前的文章已经写超过万字了。

想写出一篇好博客真的是很艰苦的一件事情(虽然我也不知道我这篇能不能被称之为好,但我觉得应该可以),要在别人研究过的东西中提取出自己的思想,拿出自己的见解。可能做研究就是这样的吧,虽说文章标题是“非硬核向”,但回过头自己读着看看,似乎完全是把人骗进来杀,干脆改成自闭向好了XD。

现在真的是感觉如释重负,可算把这么个东西码完了,之后会再慢慢修改的。如果你在学习的过程中发现了什么问题也欢迎通过邮箱或QQ联系我,当然,用GitHub直接在我的仓库下提issue也是可以的。

以上。


2021-10-19 修改

修改了文中PAC流程部分对与最后一步认证的详细过程分析,感谢松神的热心交流。