设想一下,如果世界上只有A和B知道A的密码,他们该如何用这个密码来证明自己的身份?

在现实中我们有很多办法,但是在充满了未知和陷阱的赛博空间中很多办法是统统不能用的。因为如果一方是假的,那么A的密码就失窃了。那么如果我们要做到既不说出密码,又得让对方知道自己有这个密码,该怎么办呢?

Kerberos为我们提供了一套严密的办法。

Kerberos简介

Kerberos 是Windows活动目录中使用的客户/服务器认证协议,为通信双方提供双向身份认证。

其设计目标是通过密钥系统为客户机/服务器应用程序提供强大的认证服务。该认证过程的实现不依赖于主机操作系统的认证,无需基于主机地址的信任,不要求网络上所有主机的物理安全,并假定网络上传送的数据包可以被任意地读取、修改和插入数据

名词解释(建议全文背诵,XD)

  • KDC

    Kerberos认证由三方参加:客户端,服务端(或者称为资源)和权威第三方认证。参与的权威第三方认证被称为密钥分发中心(简称KDC,KeyDistribution Center)。

    KDC有两个服务及一个数据库组成:

KDC组成

1
2
3
1. 身份验证服务(Authentication Server,简称AS)
2. 票据授予服务(Ticket Granting Server,简称TGS)
3. AD(account database)账户数据库,在这里面保存了域中所有客户端和服务器的账户信息,而且是以白名单的方式进行工作的。也就是说只有经过认证的在域控注册的账户才能正常在域中进行访问。

​ 在之后这两个服务分别称为KDC_ASKDC_TGS

​ 在Windows域环境下,身份验证服务和票据授予服务可同时运行在任何可写的域控服务器上。

​ 也就是说从物理层面来看DC(域控)≈KDC(密钥分发中心)。

  • Ticket

    在Kerberos认证中,客户端向服务端认证的凭据叫做票据(Ticket)

  • TGT

    用于向KDC_TGS申请票据的临时证明叫做TGT(Ticket Granting Ticket),直译过来就是(用于)票据授予的票据。(一听就是老套娃了)TGT是一种特殊类型的Ticket,可用于获取其他Ticket。 TGT是在Client与身份验证服务(AS)的初始身份验证之后获得的; 此后,用户无需出示其凭据,而可以使用TGT获得后续票证。

  • SessionKey

    会话密钥,用于Client和KDC通讯的密钥,由KDC_AS服务生成,生存期为整个“会话”。

  • Server_sessionKey

    服务会话密钥,用于Client和Server通讯的密钥,由KDC_TGS服务生成。

总流程图

Kerberos流程

域认证粗略流程

1. 第一步

客户端向KDC_AS服务请求(也就是KRB_AS_REQ),希望验证自己的身份获取TGT。KDC得到了这个消息,首先得判断客户端是否是可信赖的,也就是所谓的白名单黑名单。这就是KDC_AS服务完成的工作,通过在AD中存储的黑名单和白名单来区分合法客户端。成功后,返回加密的SessionKey和TGT给客户端(KRB_AS_REP)。

2. 第二步

客户端得到了TGT后,继续向KDC_TGS请求(KRB_TGS_REQ),希望获取访问某个资源的权限。KDC又得到了这个消息,这时候通过客户端发来消息中的TGT,判断出了客户拥有了这个权限,给了客户访问资源的Ticket票据(KRB_TGS_REP)。

3. 第三步

客户得到Ticket票据后,终于可以成功访问资源。但是注意,这个Ticket票据只是针对这个资源,其他资源仍旧需要向KDC_TGS申请。

域认证详细流程

1.KRB_AS_REQ

KRB_AS_REQ

正如你在这张图中看到的,在认证流程中Client A向KDC_AS发送了KRB_AS_REQ请求(身份认证请求)。

在这个请求中,Client A向KDC_AS发送了用自己的密码NTML hash(以后就简称为Client A hash)作为密钥加密的一个时间戳,以及明文传输的Client A信息(一般为用户名)和一串随机字符串。

NTLM Hash与NTLM
在Windows中,密码Hash目前称之为NTLM Hash,其中NTLM全称是:“NT LAN Manager”。

这个NTLM是一种网络认证协议,与NTLM Hash的关系就是:NTLM网络认证协议是以NTLM Hash作为根本凭证进行认证的协议。

也就是说,NTLM与NTLM Hash相互对应。

NTLM Hash的产生
假设我的密码是admin,那么操作系统会将admin转换为十六进制,经过Unicode转换后,再调用MD4加密算法加密,这个加密结果的十六进制就是NTLM Hash

admin -> hex(16进制编码) = 61646d696e
61646d696e -> Unicode = 610064006d0069006e00
610064006d0069006e00 -> MD4 = 209c6174da490caeb422f3fa5a7ae634

引用自:https://www.secpulse.com/archives/94848.html

当KDC_AS接收到KRB_AS_REQ请求之后会发生这些事:

  • 首先AS会看到Client A的用户名,于是AS在AD中查找Client A的NTLM hash。

  • AS用查找出的Client A hash解密{时间戳}Client A hash,得到时间戳。

    因为AS可以用AD中查找出的Client A hash来解密{时间戳}Client A hash,那么AS就可以确认Client A的身份了,因为理论上来说别的账号是没有Client A的密码的。

  • AS还看到了一串随机字符串

在这里我们先了解一下为什么Client A要加密一串时间戳而不是别的什么玩意儿。

就像我在前面说的,Kerberos的设计背景之一就是“假定网络上传送的数据包可以被任意地读取、修改和插入数据”。

为了保证安全,避免黑客在网络中截获KRB_AS_REQ,然后伪装成Client A欺骗KDC(我们把这种攻击手段称为“重放攻击”)。因为重放攻击伪装也需要消耗一定时间,所以KDC在收到请求后把解密得到的时间戳和当前时间来进行对比,如果相差过大的话就可以判断为重放攻击。

这也是为什么我们需要保证Windows域中所有设备的时间同步的原因。

所以,如果不是加密时间戳而是别的东西的话,就没办法避开重放攻击了。

2. KRB_AS_REP

KRB_AS_REP

在上一步AS_REQ中,KDC_AS已经认证了Client A 的身份,现在到KDC向Client A证明自己并且向Client A发送之后服务所需材料的时候了。

KRB_AS向Client A发送了KRB_AS_REP,在这里面包含了两个信息:

  • TGT(忘了这是什么的上去翻名词解释),TGT={Client A信息,SessionKey}krbtgt hash

    krbtgt是Windows域中KDC的一个特殊用户,你可以理解为KDC的账户就是krbtgt

  • {SessionKey,时间戳,随机字符串}Client A hash

    SessionKey在总流程图中有解释

那么KDC如何向Client A证明自己的身份呢?

这时候上文我们提到的随机字符串就派上用场了。因为这串随机字符串是当初Client A生成并发送的,那么理论上来说,如果Client A用自己的密码hash从返回的数据包中解密得到这么一个随机字符串,Client A就完成了对KDC身份的认证。因为别的设备不会有这串随机字符串,更不可能用Client A hash来加密{SessionKey,时间戳,随机字符串}Client A hash

说到现在我们还没提到SessionKey这个东西,我们回顾上面的过程,似乎不需要SessionKey就已经完成了Client A和KDC的相互认证。但是SessionKey真的是多余的吗?

很明显不是的,因为程序员不会在数据包中放一串无用信息来占空间。不过这个不是真正的原因。

真正的原因是因为,如果Client每次认证都需要让KDC调出账号密码,进行hash加解密还有各种操作,那么KDC将会非常忙碌。而且域中肯定不会只有一个Client,而且每个Client可能每天要验证数十次。

这活儿如果要一个人去做你还不如干脆sa了他,对于KDC来说也是很难负担得起的。所以Kerberos设计了一个巧妙的办法来解决KDC的这一部分压力问题。

KDC生成了两把一样的密钥SessionKey(为什么叫SessionKey看上面的总流程图,有解释),作为之后Client A和KDC之间互相认证用。因为如果KDC在Client A发来的请求中提取到了这个SessionKey,那么就能证明Client A的身份,免去了从AD中调出Client A的密码以及进行hash加解密的工作。

按照常理来说,这两把密钥中应该有一把保留在KDC,就像是宿舍的锁总是要有一把钥匙在宿管一样。但是对于KDC来说保留这个密钥也是一个负担,于是KDC将这个应该留下的密钥委托给Client A保管。之后如果Client A需要和KDC验证身份的时候只需要交还这把密钥就行了。

这个办法乍一听不太靠谱,因为Client A可能被假冒,如果假冒的Client A交还回来一把假密钥怎么办呢?

为了避免这个问题,KDC将这个SessionKey用自己的密码hash(也就是krbtgt hash)加密,然后委托给A。这个委托给A的密钥在Kerberos中就叫做TGT。因为Client A没有krbtgt hash,所以也就无从伪造SessionKey。

当然,如果有的话那么就可以伪造黄金票据了,这是后话暂且不表。

TGT={Client A信息,SessionKey}krbtgt hash

有了这个委托保存机制之后,KDC只需要有自己的krbtgt hash,就能够解开委托给所有Client的TGT,从而获取与这个Client进行身份验证的密钥。从而减轻了KDC的工作负担。

Client A在收到KRB_AS_REP之后,用自己的Client A hash解密{SessionKey,时间戳,随机字符串}Client A hash。通过解密提取出的时间戳和随机字符串来确定KDC的可信任,然后把SessionKey和TGT保留下来之后供接下来的认证使用。

3.KRB_TGS_REQ

KRB_TGS_REQ

在经过了与KDC_AS的身份认证之后,Client A拿到了TGT,下一步就是向KDC_TGS申请票据(Ticket)了。

首先,肯定不用说,TGT是要发送给KDC_TGS的(这也就是为什么TGT是票据授予票据)。

然后就是用在KRB_AS_REP中获取到的SessionKey加密{Client A信息,时间戳},以及要访问的Server B的相关信息。

也就是说

1
KRB_TGS_REQ=TGT,{Client A信息,时间戳}SessionKey+“Server B服务相关信息”

当KDC接收到KRB_TGS_REQ之后,首先会使用krbtgt hash解密TGT,从中提取出Client A信息和SessionKey;再用SessionKey来解密出Client A信息,时间戳。通过提取出的两个Client A信息进行比较来确定Client A的信息是否正确,时间戳避免重放攻击。

虽然表面上这两个Client A信息都是由Client A发送的,但实际上TGT中的Client A信息是KDC自己从数据库中查询出来的,所以只要对比信息是否吻合就可以确定Client A身份是否合法

4. KRB_TGS_REP

KRB_TGS_REP

一旦KDC通过了Client A的身份验证,那么KDC就要帮助Client A和Server B相互认证了。

在这一步就类似于之前KDC生成SessionKey以便于Client A和KDC_TGS通信一样,KDC_TGS生成了一对新的密钥,这里我们把它叫做Server_SessionKey。而且这两把密钥同样是用不同的hash加密后都委托给Client A的。

KDC_TGS向Client A发送了一个数据包里面包含了如下两条信息:

  • 票据(Ticket)={Server_SessionKey,Client A信息}Server B hash

KDC把其中的一把Server_SessionKey和Client A信息一起用Server B hash加密之后,形成了一个只有Server B能解密的Ticket。

我们不难发现对于Client A来说,不管是TGT还是Ticket都不是他能解密的。二者还都包含了Client A的信息和一把用于下一次通信的,只有服务提供方能解密的密钥,同时也都有着验证服务请求方身份的功能(验证身份这个后文再说)。

还有一点就是票据中的Client A信息可不单单是一些简单的基本信息,这里面包含了Client A所在的Domain Groups(域组),如果Client A属于很多个组,那么TGS_REP包会非常大。

  • {Server_SessionKey}SessionKey

这里又像之前一样出现了一层套娃,用SessionKey加密的Server_SessionKey。

Client A接收到了Ticket{Server_SessionKey}SessionKey之后,先用自己手里的SessionKey解密拿到Server_SessionKey。对于Ticket没有办法,因为Client A并没有Server B hash,只能在向Server B请求时发给Server B。

如果一个攻击者拿到了Server B hash,那么他将可以伪造白银票据,这也是后话,暂且不表。

5. KRB_AP_REQ和KRB_AP_REP

KRB_AP_REQ和KRB_AP_REP

因为这两个包涉及到的东西都比较简单,所以就放一起说了。不是因为我偷懒

现在Client A有了Server_SessionKey和Ticket,那就已经万事具备了。

Client A给Server B发送了KRB_AP_REQ,它由两个部分组成:

  • {Client A信息,时间戳}Server_SessionKey
  • Ticket={Server_SessionKey,Client A信息}Server B hash

Server B在收到KRB_AP_REQ之后用自己的Server B hash解密Ticket,从中拿到了Server_SessionKey和KDC发送的Client A信息;

再用这把Server_SessionKey解密{Client A信息,时间戳}Server_SessionKey,提取到Client A信息和时间戳。

Server B对比两个收到的Client A信息确定A的身份和权限(听着是不是很熟悉?KDC_TGS也是这么做的,只不过TGS是查询的权限)。

如果验证通过那么Server B就相信Client A身份是真的。

也就是说Server B对于Client A的认识全是基于KDC的,如果Krbtgt hash被泄露之后,那么整个域的信任关系就不复存在了。可信第三方已经不值得相信了,这波叫敌在本能寺!

时间戳作用和以前一样,都是防重放攻击的。


KRB_AP_REQ和KRB_AP_REP

Server B确定了Client A可信而且拥有申请对应服务的权限之后就要着手于给Client A提供服务了,当然,Server B同样需要向Client A证明自己的身份。

于是Server B向Client A发送了KRB_AP_REP

这个很简单

1
AP_REP={时间戳}Server_SessionKey

不过,为什么Client A看到这么简单一个回复就能证明Server B是可信的呢?

因为Server_SessionKey是包含在Ticket中的,而Ticket又是由Server B hash加密的。也就是说只有真正的Server B才能返回这个Server_SessionKey。如果返回的Server_SessionKey不对的话,Client A也就能判明返回这个数据包的Server B是假冒的。

Client A用Server_SessionKey解密拿到时间戳,确定不是重放攻击,那么认证过程就结束了。

至此所有的认证过程结束,剩下的服务就交给别的协议执行吧,Kerberos不管这个。

实际上如果我们直接在网络中抓包会发现其实并没有直接看到KRB_AP_REP和KRB_AP_REQ。这是因为Kerberos是身份认证协议,往往是其它服务协议的前提,所以说你可能会在这些服务协议的第一个请求包里找到KRB_AP_REQ,在返回包里找到KRB_AP_REP。

写在后面

这篇文章是我在学习域渗透相关知识的关于Kerberos的总结文章,算了算从开始到现在花去了半个月的时间,一开始在网上找了各种资料,但是看着都很让人自闭,决定要写一篇相对全面(?)而且更加易懂的讲Kerberos的文章。

不过还是我实力⑧行,肯定还有很多东西没讲到,有的地方应该还会有错误。如果你们在阅读过程中发现了什么错误欢迎联系我指出。

我的邮箱是:892175736@qq.com

非常感谢。

结果写文章还是把自己写自闭了。全篇下来接近5000词,也才只是讲了一下Kerberos的流程,剩下的里面一些过程漏洞的利用还没讲。只有等下篇文章了,我现在除了肚子饿就是想开着OF40去雷普锅盖头。

告辞。

更新

2024.04.15 修改了文中一处错误“形成了一个只有Server B能加密的Ticket”改为“形成了一个只有Server B能解密的Ticket”。谢谢孟老爷指出。

参考资料

《Wireshark网络分析就是这么简单》——林沛满

https://www.secpulse.com/archives/94848.html

https://www.freebuf.com/vuls/56081.html

https://www.freebuf.com/articles/system/45631.html

https://www.cnblogs.com/tonny-li/p/5466092.html(这篇写的很好,但是对于英语不好的你可能进去就感觉自己被雷普了)