第 4 章 Kerberos 认证(krb5)
asn1.py — Kerberos 数据包格式(AS/TGS/AP)
asn1.py 主要定义了 Kerberos 协议中各类请求 / 响应的数据包格式,如 AS_REQ、AS_REP、TGS_REQ、TGS_REP 等。
class AS_REP(KDC_REP):
tagSet = _application_tag(constants.ApplicationTagNumbers.AS_REP.value)
class TGS_REP(KDC_REP):
tagSet = _application_tag(constants.ApplicationTagNumbers.TGS_REP.value)
以下是 Kerberos 认证中最常见的几类结构:
AS_REP, TGS_REQ, AP_REQ, TGS_REP, Authenticator(认证类), EncASRepPart(AS请求加密部分),AuthorizationData等等
具体使用方式可以参考 examples 中涉及 Kerberos 认证的脚本(如 getST、ticketer 等),从中可以看到票据请求各阶段对 asn1.py 各数据结构的调用与赋值。
# eg.impacket/examples/goldenPac.py
def getKerberosTGS(self, serverName, domain, kdcHost, tgt, cipher, sessionKey, authTime):
......
# Key Usage 4
# TGS-REQ KDC-REQ-BODY AuthorizationData, encrypted with
# the TGS session key (Section 5.4.1)
encryptedEncodedIfRelevant = cipher.encrypt(sessionKey, 4, encodedIfRelevant, None)
tgsReq = TGS_REQ()
reqBody = seq_set(tgsReq, 'req-body')
opts = list()
opts.append( constants.KDCOptions.forwardable.value )
opts.append( constants.KDCOptions.renewable.value )
opts.append( constants.KDCOptions.proxiable.value )
reqBody['kdc-options'] = constants.encodeFlags(opts)
seq_set(reqBody, 'sname', serverName.components_to_asn1)
reqBody['realm'] = decodedTGT['crealm'].prettyPrint()
now = datetime.datetime.utcnow() + datetime.timedelta(days=1)
reqBody['till'] = KerberosTime.to_asn1(now)
reqBody['nonce'] = random.SystemRandom().getrandbits(31)
seq_set_iter(reqBody, 'etype', (cipher.enctype,))
reqBody['enc-authorization-data'] = noValue
reqBody['enc-authorization-data']['etype'] = int(cipher.enctype)
reqBody['enc-authorization-data']['cipher'] = encryptedEncodedIfRelevant
apReq = AP_REQ()
apReq['pvno'] = 5
apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value)
opts = list()
apReq['ap-options'] = constants.encodeFlags(opts)
seq_set(apReq,'ticket', ticket.to_asn1)
authenticator = Authenticator()
authenticator['authenticator-vno'] = 5
authenticator['crealm'] = decodedTGT['crealm'].prettyPrint()
clientName = Principal()
clientName.from_asn1( decodedTGT, 'crealm', 'cname')
seq_set(authenticator, 'cname', clientName.components_to_asn1)
now = datetime.datetime.utcnow()
authenticator['cusec'] = now.microsecond
authenticator['ctime'] = KerberosTime.to_asn1(now)
encodedAuthenticator = encoder.encode(authenticator)
......
constants.py — Kerberos 枚举常量(flag / error_code)
主要包含 Kerberos 认证中涉及的各类 flag、error_code、主体类型等静态枚举变量,方便认证时调用。
# eg.examples/GetUserSPNs.py
......
from impacket.examples import logger
from impacket.examples.utils import parse_credentials
from impacket.krb5 import constants
# No TGT in cache, request it
userName = Principal(self.__username, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
# eg.krb5/constants.py
class PrincipalNameType(Enum):
NT_UNKNOWN = 0
NT_PRINCIPAL = 1
NT_SRV_INST = 2
NT_SRV_HST = 3
NT_SRV_XHST = 4
NT_UID = 5
NT_X500_PRINCIPAL = 6
NT_SMTP_NAME = 7
NT_ENTERPRISE = 10
NT_WELLKNOWN = 11
NT_SRV_HST_DOMAIN = 12
NT_MS_PRINCIPAL = -128
NT_MS_PRINCIPAL_AND_ID = -129
NT_ENT_PRINCIPAL_AND_ID = -130
keytab.py — 密钥表文件解析与保存
同样顾名思义,该文件包含一系列解析或保存 keytab 文件的类与函数。keytab 是保存 Principal 身份密钥的密钥表文件,用途类似 SSH 身份认证中的 id_rsa 私钥,方便通过 Kerberos 进行身份校验,一般保存在 /etc/security/keytabs/ 下(如 nn.service.keytab)。以 CDH 生成并使用 keytab 文件为例:
1、进入到kerberos
kadmin.local
2、查看kerberos成员
listprincs
3、添加kerberos成员
kadmin -p 'kdcadmin/admin' -w "-s" -q 'addprinc -randkey hive'
4、生成keytab文件
ktadd -k /home/kerberos/hive.keytab -norandkey hive@TEST.COM
5、使用生成的keytab文件认证用户
kinit -kt /home/kerberos/hive.keytab hive/bdp4@TEST.COM
6、查看当前认证用户
klist
7、使用beeline远程访问
beeline -u "jdbc:hive2://1*92.168.86.130:10000/default;principal=hive/bdp4@TEST.COM"
keytab文件格式如下
keytab {
uint16_t file_format_version; /* 0x502 */
keytab_entry entries[*];
};
keytab_entry {
int32_t size;
uint16_t num_components; /* sub 1 if version 0x501 */
counted_octet_string realm; 域名
counted_octet_string components[num_components]; 主体名称
uint32_t name_type; /* not present if version 0x501 */ 主体类型
uint32_t timestamp; 时间戳
uint8_t vno8; 密钥版本号
keyblock key;
uint32_t vno; /* only present if >= 4 bytes left in entry */
};
counted_octet_string {
uint16_t length;
uint8_t data[length];
};
keyblock {
uint16_t type; 加密类型
counted_octet_string;加密key
};
在 keytab 类的 getData()、getKey() 函数中,可以看到对 keytab 文件中数据结构的解析和取值。
ccache.py — 凭据缓存文件解析(toTGT / toTGS)
正如第 2 章 structure.py 中所见,ccache.py 是对 Kerberos 凭据二进制缓存文件(ccache)进行解析的类,包含 Credential 中的 toTGT、toTGS 等函数。先看下 ccache 缓存文件的结构:
ccache {
uint16_t file_format_version; /* 0x0504 */ 文件格式版本
uint16_t headerlen; /* only if version is 0x0504 */
header headers[]; /* 仅 0x0504 及以上版本存在 */
principal primary_principal;
credential credentials[*];
};
# header 结构为「tag + taglen + tagdata」;最常用 tag 为 DeltaTime(0x0001),
# tagdata 中是 time_offset / usec_offset,用于本机时钟与 KDC 的时间偏移校准
credential {
principal client; 客户端数据块
principal server; 服务端数据块
keyblock key; 密钥块
times time; 时间模块(authtime / starttime / endtime / renew_till)
uint8_t is_skey; 是否是skey /* 1 if skey, 0 otherwise */
uint32_t tktflags; /* stored in reversed byte order */
uint32_t num_address;
address addrs[num_address]; 地址模块
uint32_t num_authdata;
authdata authdata[num_authdata]; 授权数据
counted_octet_string ticket; 票据
counted_octet_string second_ticket; 第二张票据,通过 DUPLICATE-SKEY 或 ENC-TKT-IN-SKEY 与票据相关
};
keyblock {
uint16_t keytype;加密类型
uint16_t etype; /* only present if version 0x0503 */
uint16_t keylen;
uint8_t keyvalue[keylen]; 密钥key
};
principal {
uint32_t name_type; /* not present if version 0x0501 */
uint32_t num_components; /* sub 1 if version 0x501 */
counted_octet_string realm; 域
counted_octet_string components[num_components]; 用户/服务名称
};
# times / address / authdata / counted_octet_string 子结构略:
# 均为「类型字段 + counted_octet_string(length + data)」的两段式布局
第二张票据的存在初看令人费解,查了很多文档后,在 IBM 的系统编程文档中找到了它的应用场景: https://www.ibm.com/docs/en/zos/2.3.0?topic=kpi-krb5-get-cred-from-kdc-obtain-kdc-server-service-ticket
#include <skrb/krb5.h>
krb5_error_code krb5_get_cred_from_kdc (
krb5_context context,
krb5_ccache ccache,
krb5_creds * in_cred,
krb5_creds ** out_cred,
krb5_creds *** tgts
);
Input
context
Specifies the Kerberos context.
ccache
Specifies the credentials cache. The initial TGT for the local realm must already be in the cache. The Kerberos runtime obtains additional ticket-granting tickets as needed if the target server is not in the local realm.
in_cred
Specifies the request credentials. The client and server fields must be set to the desired values for the service ticket. The second_ticket field must be set if the service ticket is to be encrypted in a session key. The ticket expiration time can be set to override the default expiration time.
如果要在会话密钥中加密服务票证,则必须设置second_ticket字段。
Output
out_cred
Returns the service ticket. The krb5_free_creds() routine should be called to release the credentials when they are no longer needed.
tgts
Returns any new ticket-granting tickets that were obtained while getting the service target from the KDC in the target realm. There may be ticket-granting tickets returned for this parameter even if the Kerberos runtime was ultimately unable to obtain a service ticket from the target KDC. The krb5_free_tgt_creds() routine should be called to release the TGT array when it is no longer needed.
注意区分概念:IBM 文档中的 second_ticket 是 krb5_get_cred_from_kdc() 的输入字段,仅在需要将服务票证与会话密钥绑定等特殊场景下才要求设置;而 impacket 的 CCache.secondTicket 是解析 / 保存 ccache 时的本地结构字段。fromKRBCRED() 中将其初始化为空,只代表当前实现未读取该内容,并不代表 IBM API 语义上默认为空:
def fromKRBCRED(self, encodedKrbCred):
.........
credential.ticket['length'] = len(credential.ticket['data'])
credential.secondTicket = CountedOctetString()
credential.secondTicket['data'] = b''
credential.secondTicket['length'] = 0
在impacket模块中,该类主要用于读取或保存ccache缓存文件,其中重要的为如下函数
def toKRBCRED(self):
def fromKRBCRED(self, encodedKrbCred):
def loadKirbiFile(cls, fileName):
def saveKirbiFile(self, fileName):
def fromTGS(self, tgs, oldSessionKey, sessionKey):
def fromTGT(self, tgt, oldSessionKey, sessionKey):
def getCredential(self, server, anySPN=True):
如以下示例
# eg./examples/getTGT.py
def saveTicket(self, ticket, sessionKey):
logging.info('Saving ticket in %s' % (self.__user + '.ccache'))
from impacket.krb5.ccache import CCache
ccache = CCache()
ccache.fromTGT(ticket, sessionKey, sessionKey)
ccache.saveFile(self.__user + '.ccache')
types.py — Principal 等认证数据处理类
主要是 KerberosException、Principal、Address、EncryptedData、Ticket、KerberosTime 等 Kerberos 认证中常用数据的处理类。其中最重要的是 Principal(认证主体):它由三部分组成——primary(用户 / 服务名)、instance(服务实例名)和 realm(域名)。primary 与 instance 之间用 / 分隔,instance 与 realm 之间用 @ 分隔,如 joe/admin@EXAMPLE.COM 或 joe/node2.example.com。
具体Principal格式解析如下
class Principal(object):
"""The principal's value can be supplied as:
* a single string
* a sequence containing a sequence of component strings and a realm string
* a sequence whose first n-1 elemeents are component strings and whose last
component is the realm
If the value contains no realm, then default_realm will be used."""
def __init__(self, value=None, default_realm=None, type=None):
......
elif isinstance(value, str):
# 字符串形式:正则拆出 realm(@ 之后)与各组件(/ 分隔,支持 \ 转义)
m = re.match(r'((?:[^\\]|\\.)+?)(@((?:[^\\@]|\\.)+))?$', value)
if not m:
raise KerberosException("invalid principal syntax")
def unquote_component(comp):
return re.sub(r'\\(.)', r'\1', comp)
if m.group(2) is not None:
self.realm = unquote_component(m.group(3))
else:
self.realm = default_realm
self.components = [
unquote_component(qc)
for qc in re.findall(r'(?:[^\\/]|\\.)+', m.group(1))]
elif len(value) == 2:
......
# __eq__ / __str__ / __repr__ 等方法略。
# from_asn1 与 components_to_asn1 负责 Principal 与 asn1 结构的互转,
# 在 getKerberosTGT / getKerberosTGS 中大量使用(见本章 kerberosv5.py 一节)
在getST中我们可以看到对Principal的赋值
principal = ccache.credentials[0].header['server'].prettyPrint()
crypto.py — Kerberos 加密类型实现(RC4 / AES)
实现了 Kerberos 各加密类型(RC4-HMAC、AES128/256-CTS-HMAC-SHA1 等)的加解密与密钥推导(string_to_key)函数,MD4 / MD5 等算法用于支撑 RC4 等加密类型。
这边顺便说下,impacket 根目录下还有一个同名的 crypto.py,实现了 AES-CMAC-PRF-128、AES-CMAC 等通用算法,smb3.py 与 ccache.py 中都能看到对这两个加密模块的调用,secretsdump.py 也同时用到了两边的加密函数。两个文件分工不同:krb5/crypto.py 面向 Kerberos 加密类型,根目录 crypto.py 面向通用协议场景(如 SMB3 签名用的 AES-CMAC)。
# eg.impacket/krb5/ccache.py
from impacket.krb5 import crypto, constants, types
.......
seq_set(tgt_rep,'ticket', ticket.to_asn1)
cipher = crypto._enctype_table[self['key']['keytype']]()
tgt = dict()
tgt['KDC_REP'] = encoder.encode(tgt_rep)
# eg.impacket/smb3.py
from Cryptodome.Cipher import AES
from impacket import nmb, ntlm, uuid, crypto
........
if len(self._Session['SessionKey']) > 0:
p = packet.getData()
signature = crypto.AES_CMAC(self._Session['SigningKey'], p, len(p))
gssapi.py — GSS-API 封装(MIC / WRAP)
GSSAPI 是 RFC 2743 中定义的安全认证工业标准接口,常用于服务的 Kerberos 认证,如 MongoDB、PostgreSQL、FTP 等。这里需要理解 GSSAPI 与 Kerberos 认证协议的区别:GSSAPI 的全称是Generic Security Services Application Program Interface(通用安全服务应用程序接口)。可以看出,GSS-API 是一个 API 规范,是实现 Kerberos 协议时定义的编程接口。
The dominant GSSAPI mechanism implementation in use is Kerberos. Unlike the GSSAPI, the Kerberos API has not been standardized and various existing implementations use incompatible APIs. The GSSAPI allows Kerberos implementations to be API compatible.
意思是:有了这个规范,只要各厂商按规范实现 Kerberos,至少可以做到一个客户端能连接不同厂商的 KDC 服务;反过来,一个服务端也能对接不同实现方式的客户端。
总结下来就是:Kerberos 是密码学家从理论上设计出的认证协议,GSS-API 是架构师为实现该协议设计的程序接口,MIT Kerberos、Windows AD 等厂商通过实现 GSS-API 做到了 API 层面的兼容——即基于 GSSAPI 实现 Kerberos 协议通信。而 SPNEGO(Simple and Protected GSS-API Negotiation,RFC 4178)是一种在多种 GSS-API 机制间进行协商选择的机制,微软在 SMB、HTTP 认证等场景中广泛用它来在 Kerberos 与 NTLM 之间协商,实现 Windows 凭据的传递共享

具体协议标准可以看https://datatracker.ietf.org/doc/html/rfc4121 The Kerberos Version 5 Generic Security Service Application Program Interface (GSS-API) Mechanism: Version 2
gssapi.py 中实现了 RC4 与 AES 两种加密形式的 GSSAPI。
其中 MIC 结构指 Message Integrity Code(消息完整性校验),用于防止篡改。客户端发送 ISC_REQ_NO_INTEGRITY 可显式关闭该校验;另一种方式是初次调用 InitializeSecurityContext 时,由服务端通过 NegpDetermineTokenPackage 函数决定是否启用消息完整性校验——若 initial token 是 NTLM 或 Kerberos token,则不会开启 ISC_REQ_INTEGRITY;若发起方是 SMB 协议,该标志位默认为 1,服务端会选择签名。而 CVE-2019-1040 漏洞可绕过 NTLM MIC 的防护机制,使我们能修改标志位、让服务器不进行 LDAP 签名(典型组合:Exchange + CVE-2019-1040 接管全域,或 RBCD + PrinterBug/PetitPotam + CVE-2019-1040)。
WRAP 结构则用于保密性:GSSAPI 初始化 token 建立上下文后,后续通信数据都通过 wrap / unwrap 进行加解密。



GSSAPI 的关键 C 函数如下(注意实际 API 为全小写 gss_ 前缀):
gss_acquire_cred Obtains the user's identity proof, often a secret cryptographic key
gss_import_name Converts a username or hostname into a form that identifies a security entity
gss_init_sec_context Generates a client token to send to the server, usually a challenge
gss_accept_sec_context Processes a token from gss_init_sec_context and can generate a response token to return
gss_wrap Converts application data into a secure message token (typically encrypted)
gss_unwrap Converts a secure message token back into application data
下面是使用 GSS-API 的一般步骤:
-
每个应用程序(无论是发送者还是接受器)都明确获取凭证,除非已经自动获取凭证。使用 gss_acquire_cred() 或 gss_add_cred() 函数
-
发送者启动一个安全上下文, 接受器接受该上下文。gss_init_sec_context() 函数用于启动应用程序和远程服务器之间的安全上下文。 如果成功,该函数将返回一个上下文句柄以建立上下文,还将返回一个要发送到接受器的上下文级别的令牌。 调用 gss_init_sec_context() 之前,客户机应当执行以下任务:
-
使用 gss_acquire_cred() 获取凭证(如有必要)。 通常,客户机会在登录时接收凭证。gss_acquire_cred() 只能从正在运行的操作系统中检索初始凭证。
- 使用 gss_import_name() 以 GSS-API 内部格式导入服务器的名称。 有关名称和 gss_import_name() 的更多信息,请参见GSS-API 中的名称。
调用 gss_init_sec_context() 时,客户机通常会传递以下参数值:
GSS_C_NO_CREDENTIAL(用于 cred_handle 参数),用于表示缺省凭证GSS_C_NULL_OID(用于 mech_type 参数),用于表示缺省机制GSS_C_NO_CONTEXT(用于 context_handle 参数),用于表示初始的空上下文。 由于 gss_init_sec_context() 通常会循环调用,因此后续的调用会传递以前的调用所返回的上下文句柄GSS_C_NO_BUFFER(用于 input_token 参数),用于表示最初为空的令牌。 或者,应用程序可以传递一个指向 gss_buffer_desc 对象的指针,该对象的长度字段已经设置为零- 使用 gss_import_name() 以 GSS-API 内部格式导入的服务器名称。
- 上下文接受器可能需要多次握手才能建立上下文。也就是说,接受器会要求发起方先发送多段上下文信息,再建立完整上下文。因此为了可移植性,应始终在检查上下文是否完全建立的循环中发起上下文。
-
建立上下文的另一面是接受上下文,通过 gss_accept_sec_context() 函数完成。通常由服务器接受客户机用 gss_init_sec_context() 已启动的上下文。输出令牌通过 gss_accept_sec_context() 返回,之后调用时输入令牌作为参数传入;当不再向发起方发送令牌时,该函数返回长度为零的输出令牌。除检查返回状态外,循环还应检查输出令牌长度,判断是否必须发送其他令牌。开始循环之前,应将输出令牌长度初始化为零(设为
GSS_C_NO_BUFFER,或将结构长度字段置零)。 -
发送者向要传送的数据应用安全保护机制。 发送者会对消息进行加密或者使用标识标记对数据进行标记。 发送者随后将传送受保护的消息。
注 –
发送者可以选择不应用安全保护机制,在这种情况下,消息仅具有缺省的 GSS-API 安全服务,即验证。
-
接受器根据需要对消息进行解密,并在适当的情况下对消息进行验证。
-
(可选)接受器将标识标记返回到发送者进行确认。
-
这两个应用程序都会销毁共享的安全上下文。 如有必要,分配功能还可以解除分配其余任何 GSS-API 数据。
在impacket中主要使用gssapi中涉及到的静态变量
# eg.impacket/krb5/kerberosv5.py
from impacket.krb5.gssapi import CheckSumField, GSS_C_DCE_STYLE, GSS_C_MUTUAL_FLAG, GSS_C_REPLAY_FLAG,
gssapi.py
# Constants
GSS_C_DCE_STYLE = 0x1000
GSS_C_DELEG_FLAG = 1
GSS_C_MUTUAL_FLAG = 2
GSS_C_REPLAY_FLAG = 4
GSS_C_SEQUENCE_FLAG = 8
GSS_C_CONF_FLAG = 0x10
GSS_C_INTEG_FLAG = 0x20
# Mic Semantics
GSS_HMAC = 0x11
# Wrap Semantics
GSS_RC4 = 0x10
# 2. Key Derivation for Per-Message Tokens
KG_USAGE_ACCEPTOR_SEAL = 22
KG_USAGE_ACCEPTOR_SIGN = 23
KG_USAGE_INITIATOR_SEAL = 24
KG_USAGE_INITIATOR_SIGN = 25
KRB5_AP_REQ = struct.pack('<H', 0x1)
# 1.1.1. Initial Token - Checksum field
class CheckSumField(Structure):
structure = (
('Lgth','<L=16'),
('Bnd','16s=b""'),
('Flags','<L=0'),
)
spnego.py(位于 impacket 根目录)
这里顺便看下 SPNEGO 协议。需要先澄清:SPNEGO 并不是“微软对 Kerberos 的扩展”,而是 IETF 定义的 GSS-API 协商机制(RFC 4178),用于在客户端与服务端之间协商出实际使用的安全机制(如 Kerberos 或 NTLM);微软在 SMB、HTTP 等协议中广泛使用它。
kerberos认证协议如下

SPNEGO协议认证如下

- 首先,用户从工作站登录到 Microsoft 域控制器
MYDOMAIN.EXAMPLE.COM。 - 然后,用户尝试访问 Web 应用程序。 用户使用客户机浏览器请求受保护的 Web 资源,这会将
HTTP GET请求发送到 Liberty 服务器。 - Liberty 服务器中的 SPNEGO 认证使用包含
Authenticate: Negotiate状态的HTTP 401验证问题头来回答客户机浏览器。 - 客户机浏览器识别协商头,因为客户机浏览器配置为支持集成 Windows 认证。 客户机针对主机名解析所请求的 URL。 客户机使用主机名来构成目标 Kerberos 服务主体名称 (SPN)
HTTP/myLibertyMachine.example.com,以从 Microsoft Kerberos KDC (TGS_REQ) 中的 Kerberos 授予凭单的服务 (TGS) 请求 Kerberos 服务凭单。 然后,TGS向客户机发出 Kerberos 服务凭单 (TGS_REP)。 Kerberos 服务凭单 (SPNEGO 令牌) 证明用户的身份和对服务 (Liberty 服务器) 的许可权。 - 然后,客户机浏览器响应 Liberty 服务器 Authenticate: 与在请求 HTTP 头的上一步中获取的 SPNEGO 令牌协商提问。
- Liberty 服务器中的 SPNEGO 认证会看到带有 SPNEGO 令牌的 HTTP 头,验证 SPNEGO 令牌,并获取用户的身份 (主体)。
- 在 Liberty 服务器获取用户身份后,它将验证其用户注册表中的用户并执行授权检查。
- 如果授予了访问权,那么 Liberty 服务器将使用
HTTP 200发送响应。 Liberty 服务器还在响应中包含 LTPA cookie。 此 LTPA Cookie 用于后续请求。
在impacket中常用的主要是SPNEGO_NegTokenInit, TypesMech, SPNEGO_NegTokenResp, ASN1_AID,主要使用在ntlmrelayx中继攻击中,在https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/06451bf2-578a-4b9d-94c0-8ce531bf14c4中可以看到smb调用MS-SPNG(SPNEGO)来验证用户

认证流程如下

在spnego.py,中常用的也是SPNEGO_NegTokenInit类和SPNEGO_NegTokenResp类来解析修改smb auth请求
# eg./impacket/examples/ntlmrelayx/clients/smtprelayclient.py
from impacket.spnego import SPNEGO_NegTokenResp
......
def sendAuth(self, authenticateMessageBlob, serverChallenge=None):
if unpack('B', authenticateMessageBlob[:1])[0] == SPNEGO_NegTokenResp.SPNEGO_NEG_TOKEN_RESP:
respToken2 = SPNEGO_NegTokenResp(authenticateMessageBlob)
token = respToken2['ResponseToken']
else:
token = authenticateMessageBlob
auth = base64.b64encode(token)
self.session.putcmd(auth)
typ, data = self.session.getreply()
if typ == 235:
self.session.state = 'AUTH'
return None, STATUS_SUCCESS
else:
LOG.error('SMTP: %s' % ''.join(data))
return None, STATUS_ACCESS_DENIED
kerberosv5.py — getKerberosTGT / getKerberosTGS 认证流程
文件中最重要的函数是 Kerberos 认证流程中的 getKerberosTGT 与 getKerberosTGS。
getKerberosTGT 首先初始化 AS_REQ,设置 pvno、msg-type 等 header 参数,再填充 req-body 中的 sname、cname 等字段:
def getKerberosTGT(clientName, password, domain, lmhash, nthash, aesKey='', kdcHost=None, requestPAC=True):
....
asReq = AS_REQ()
domain = domain.upper()
serverName = Principal('krbtgt/%s'%domain,
type=constants.PrincipalNameType.NT_PRINCIPAL.value)
....
asReq['pvno'] = 5
asReq['msg-type'] = int(constants.ApplicationTagNumbers.AS_REQ.value)
....
reqBody = seq_set(asReq, 'req-body')
....
reqBody['till'] = KerberosTime.to_asn1(now)
reqBody['rtime'] = KerberosTime.to_asn1(now)
....
if aesKey != b'':
if len(aesKey) == 32:
supportedCiphers = (int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value),)
.....
设置好 aesKey 后,通过 sendReceive 发送请求。由于第一次请求未携带预认证数据(preAuth=False),若 KDC 要求预认证,会返回 KRB_ERROR:
try:
r = sendReceive(message, domain, kdcHost)
....
try:
asRep = decoder.decode(r, asn1Spec = KRB_ERROR())[0]
....

之后用用户密钥(NTLM hash 派生 / AES key)加密时间戳,重新构建带 PADATA 的 AS_REQ:
if isinstance(nthash, bytes) and nthash != b'':
key = Key(cipher.enctype, nthash)
elif aesKey != b'':
key = Key(cipher.enctype, aesKey)
else:
key = cipher.string_to_key(password, encryptionTypesData[enctype], None)
if preAuth is True:
if enctype in encryptionTypesData is False:
raise Exception('No Encryption Data Available!')
# Let's build the timestamp
timeStamp = PA_ENC_TS_ENC()
now = datetime.datetime.utcnow()
timeStamp['patimestamp'] = KerberosTime.to_asn1(now)
timeStamp['pausec'] = now.microsecond
# Encrypt the shyte
encodedTimeStamp = encoder.encode(timeStamp)
# Key Usage 1
# AS-REQ PA-ENC-TIMESTAMP padata timestamp, encrypted with the
# client key (Section 5.2.7.2)
encriptedTimeStamp = cipher.encrypt(key, 1, encodedTimeStamp, None)
encryptedData = EncryptedData()
encryptedData['etype'] = cipher.enctype
encryptedData['cipher'] = encriptedTimeStamp
encodedEncryptedData = encoder.encode(encryptedData)
# Now prepare the new AS_REQ again with the PADATA
# ToDo: cannot we reuse the previous one?
asReq = AS_REQ()
asReq['pvno'] = 5
asReq['msg-type'] = int(constants.ApplicationTagNumbers.AS_REQ.value)
asReq['padata'] = noValue
asReq['padata'][0] = noValue
asReq['padata'][0]['padata-type'] = int(constants.PreAuthenticationDataTypes.PA_ENC_TIMESTAMP.value)
asReq['padata'][0]['padata-value'] = encodedEncryptedData
asReq['padata'][1] = noValue
asReq['padata'][1]['padata-type'] = int(constants.PreAuthenticationDataTypes.PA_PAC_REQUEST.value)
asReq['padata'][1]['padata-value'] = encodedPacRequest
......
这里直接把 AS_REP 返回包当作 tgt 变量返回。examples 中的 getTGT 脚本在调用 getKerberosTGT 后,会用 ccache.py 的 fromTGT 从返回包中提取票据,并保存为 ccache 缓存文件:
# eg./examples/getTGT.py
def run(self):
userName = Principal(self.__user, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain,unhexlify(self.__lmhash),unhexlify(self.__nthash), self.__aesKey,self.__kdcHost)
self.saveTicket(tgt,oldSessionKey)
def saveTicket(self, ticket, sessionKey):
logging.info('Saving ticket in %s' % (self.__user + '.ccache'))
from impacket.krb5.ccache import CCache
ccache = CCache()
ccache.fromTGT(ticket, sessionKey, sessionKey)
ccache.saveFile(self.__user + '.ccache')
当然,函数不止返回 AS_REP 包,还会用用户密钥解密出会话密钥:
try:
plainText = cipher.decrypt(key, 3, cipherText)
encASRepPart = decoder.decode(plainText, asn1Spec = EncASRepPart())[0]
# Get the session key and the ticket
cipher = _enctype_table[encASRepPart['key']['keytype']]
sessionKey = Key(cipher.enctype,encASRepPart['key']['keyvalue'].asOctets())
.......
if isinstance(nthash, bytes) and nthash != b'':
key = Key(cipher.enctype, nthash)

接下来看下 AS_REQ 的请求头和 req-body 究竟是什么:

1.pvno kerberos的版本号
2.msg-type 消息类型,这里就是KRB_AS_REQ(0x0a)
3.PA_DATA Pre-authentication Data,预身份认证,每个认证消息有type和value。
PA-DATA PA-ENC-TIMESTAMP 用户HASH加密后的时间戳
padata-type: padata类型
padata-value: padata的值
etype: 加密类型
cipher: 加密后的值
PA-DATA PA-PAC-REQUEST:PAC扩展
padata-type: padata类型
padata-value: padata的值
include-pac: 是否包含PAC,如果包含那么在响应包中就会返回PAC
4.req-body 请求体
padding:填充
kdc-options:用于与KDC约定一些选项设置
cname: 客户端用户名
realm: 域名
sname: 服务端用户名,在AS_REQ 中sname是krbtgt,类型是KRB_NT_SRV_INST
till: 到期时间,rubeus和kekeo都是20370913024805Z,可以作为特征检测
nonce:随机生成的一个数,用于检测重放攻击
etype: 协商加密类型,KDC按照etype类型选择用户hash对应的加密方式
返回的 AS_REP 响应包中,可以看到 TGT 票据以及用用户密钥加密的会话密钥(enc-part)。
接下来看 getKerberosTGS 函数。
它首先根据提供的 TGT 和 session key 构建 TGS_REQ(AP_REQ 中携带 TGT,以及用 session key 加密的 Authenticator):
try:
decodedTGT = decoder.decode(tgt, asn1Spec = AS_REP())[0]
except:
decodedTGT = decoder.decode(tgt, asn1Spec = TGS_REP())[0]
domain = domain.upper()
# Extract the ticket from the TGT
ticket = Ticket()
ticket.from_asn1(decodedTGT['ticket'])
....
now = datetime.datetime.utcnow()
authenticator['cusec'] = now.microsecond
authenticator['ctime'] = KerberosTime.to_asn1(now)
encodedAuthenticator = encoder.encode(authenticator)
# Key Usage 7
# TGS-REQ PA-TGS-REQ padata AP-REQ Authenticator (includes
# TGS authenticator subkey), encrypted with the TGS session
# key (Section 5.5.1)
encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 7, encodedAuthenticator, None)
KDC 收到内容 1(TGT)与内容 2(session key 加密的 Authenticator)后:先用 krbtgt 密钥解密 TGT,取出客户端身份与会话密钥;再用会话密钥解密内容 2,得到 Authenticator 中的客户端身份;比对两者一致则认证通过。随后 KDC 用内容 1 中请求的目标服务对应的密钥加密新票据,返回给客户端两部分内容:
内容 1:用目标服务密钥加密的服务票据(包含客户端 ID、客户端网络地址、有效期和客户端 / 服务端会话密钥)
内容 2:用 TGS 会话密钥加密的客户端 / 服务端会话密钥
客户端用 TGS 会话密钥解密内容 2,得到新的客户端 / 服务端会话密钥:
tgs = decoder.decode(r, asn1Spec = TGS_REP())[0]
cipherText = tgs['enc-part']['cipher']
# Key Usage 8
# TGS-REP encrypted part (includes application session
# key), encrypted with the TGS session key (Section 5.4.2)
plainText = cipher.decrypt(sessionKey, 8, cipherText)
encTGSRepPart = decoder.decode(plainText, asn1Spec = EncTGSRepPart())[0]
newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue'].asOctets())
后续的TGS的解析同样在getST脚本中利用ccache的fromTGS函数进行解析
def saveTicket(self, ticket, sessionKey):
logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache'))
ccache = CCache()
ccache.fromTGS(ticket, sessionKey, sessionKey)
ccache.saveFile(self.__saveFileName + '.ccache')
pac.py — 特权属性证书 PAC 结构与解析
特权属性证书(PAC)由完成身份验证的协议使用,用于传输授权信息、控制对资源的访问。Kerberos 协议 [RFC4120] 本身不提供授权,创建 PAC 正是为了给 Kerberos 协议扩展 [MS-KILE] 补充这部分授权数据。PAC 结构按 [MS-KILE] 对授权信息编码,包括组成员身份、附加凭证信息、配置文件与策略信息以及支持的安全元数据。

模块主要提供的是pac数据结构的支持,具体数据结构如下
class KERB_SID_AND_ATTRIBUTES(NDRSTRUCT):
# 表示用于身份验证的SID及其 属性。它在KERB_VALIDATION_INFO结构中发送,用于包含有关 SID 引用的组的附加信息。
class KERB_SID_AND_ATTRIBUTES_ARRAY(NDRUniConformantArray):
class PKERB_SID_AND_ATTRIBUTES_ARRAY(NDRPOINTER):
class DOMAIN_GROUP_MEMBERSHIP(NDRSTRUCT):
# 结构标识账户所属的域和组。它在PAC_DEVICE_INFO结构中发送。
class DOMAIN_GROUP_MEMBERSHIP_ARRAY(NDRUniConformantArray):
class PDOMAIN_GROUP_MEMBERSHIP_ARRAY(NDRPOINTER):
class PACTYPE(Structure):
# PACTYPE结构是 PAC 的最顶层结构,指定 PAC_INFO_BUFFER数组中的元素数 。PACTYPE结构用作完整 PAC 数据的标头。
class PAC_INFO_BUFFER(Structure):
# 在PACTYPE结构之后是一个PAC_INFO_BUFFER结构数组,每个结构都定义了 PAC 缓冲区的类型和字节偏移量。PAC_INFO_BUFFER数组 没有定义的顺序。因此,PAC_INFO_BUFFER 缓冲区的顺序没有意义。但是,一旦密钥分发中心 (KDC) 和服务器签名生成,缓冲区的顺序不得更改,否则 PAC 内容的签名验证将失败。
class KERB_VALIDATION_INFO(NDRSTRUCT):
# KERB_VALIDATION_INFO结构定义了 DC 提供的用户登录和授权信息。指向 KERB_VALIDATION_INFO结构的指针被序列化为一个字节数组,然后放置在最顶层PACTYPE 结构的Buffers数组之后,位于缓冲区中相应PAC_INFO_BUFFER 结构的Offset字段中指定的偏移量处。相应的PAC_INFO_BUFFER 结构的ulType字段设置为 0x00000001。
# KERB_VALIDATION_INFO结构是 NETLOGON_VALIDATION_SAM_INFO4 结构的子集(出于历史原因及 Active Directory 生成此信息的方式)。NTLM 在服务器与域控制器交换时使用 NETLOGON_VALIDATION_SAM_INFO4 结构,因此 KERB_VALIDATION_INFO 也包括特定于 NTLM 的字段;两结构共有的字段以及特定于 NTLM 身份验证操作的字段,不用于 [MS-KILE] 验证。KERB_VALIDATION_INFO 结构由 RPC [MS-RPCE] 编组。
class PKERB_VALIDATION_INFO(NDRPOINTER):
class PAC_CREDENTIAL_INFO(Structure):
# PAC_CREDENTIAL_INFO结构用作凭证信息的标头。PAC_CREDENTIAL_INFO标头指示用于加密其后数据的加密算法。后面的数据是加密的、IDL序列化的PAC_CREDENTIAL_DATA结构,其中包含用户的实际凭证。请注意,此结构不能被[MS-KILE] 协议以外的协议使用;加密方法依赖于 Kerberos AS-REQ。PAC_CREDENTIAL_INFO结构包含用户的加密凭证。使用的加密密钥是 AS 回复密钥。仅当使用 PKINIT时才包含 PAC 凭据缓冲区。因此,AS reply key 是基于PKINIT 推导出来的。
class SECPKG_SUPPLEMENTAL_CRED(NDRSTRUCT):
# 定义了需要补充凭证的安全包的名称以及该包的凭证缓冲区。
class SECPKG_SUPPLEMENTAL_CRED_ARRAY(NDRUniConformantArray):
class PAC_CREDENTIAL_DATA(NDRSTRUCT):
# 定义了一组提供给 Kerberos 客户端的特定于安全包的凭证。
class NTLM_SUPPLEMENTAL_CREDENTIAL(NDRSTRUCT):
# 用于对 NTLM 安全协议使用的凭据进行编码,特别是LAN Manager哈希(LM OWF)和NT哈希(NT OWF).PAC 结构规范中未解决生成以该结构编码的哈希值的问题。[MS-NLMP]中指定了有关如何创建哈希的详细信息。仅当使用 PKINIT [MS-PKCA] 对用户进行身份验证时,才会包含 PAC 缓冲区类型。NTLM_SUPPLEMENTAL_CREDENTIAL 结构由RPC [MS-RPCE]封送。
class PAC_CLIENT_INFO(Structure):
# 是PAC的可变长度缓冲区,其中包含客户端的名称和身份验证时间。它用于验证 PAC 是否对应于票据的客户端。PAC_CLIENT_INFO 结构直接放置在最顶层 PACTYPE 结构的 Buffers 数组之后,位于Buffers数组中相应PAC_INFO_BUFFER结构的Offset字段中 指定的偏移处。相应的PAC_INFO_BUFFER的ulType字段 设置为 0x0000000A。
class PAC_SIGNATURE_DATA(Structure):
# 两个PAC_SIGNATURE_DATA结构附加到存储服务器和KDC签名的 PAC。这些结构位于最顶层PACTYPE 结构的Buffers数组之后,位于Buffers数组中每个相应PAC_INFO_BUFFER 结构的Offset字段中指定的偏移处 。服务端签名对应的PAC_INFO_BUFFER的ulType字段包含值0x00000006和PAC_INFO_BUFFER的ulType字段对应于 KDC 签名包含值 0x00000007。只有当 PAC 被[MS-KILE] 协议使用时才能生成 PAC 签名,因为用于创建和验证签名的密钥是 KDC 已知的密钥。没有其他协议可以使用这些 PAC 签名。
class S4U_DELEGATION_INFO(NDRSTRUCT):
# S4U_DELEGATION_INFO结构用于约束委托信息。它列出了通过此 Kerberos 客户端和后续服务或服务器委托的服务。该列表仅用于用户代理服务 (S4U2proxy)请求。此功能可以在服务之间连续使用多次,这对于审计目的很有用。
class UPN_DNS_INFO(Structure):
# 包含客户端的 UPN、 完全限定的域名 (FQDN)、SAM 名称(可选)和 SID(可选)。它用于提供与票据的客户端对应的 UPN、FQDN、SAM 名称和 SID。UPN_DNS_INFO结构直接放置在最顶层 PACTYPE 结构的缓冲区数组之后,位于缓冲区数组中相应 PAC_INFO_BUFFER结构的偏移字段中指定的偏移处 。对应PAC_INFO_BUFFER的ulType字段设置为 0x0000000C。
class PAC_CLIENT_CLAIMS_INFO(Structure):
# 是 PAC 的可变长度缓冲区,应该包含客户端的编组声明 blob。PAC_CLIENT_CLAIMS_INFO 结构直接放置在最顶层 PACTYPE结构的Buffers 数组之后,位于Buffers数组中相应PAC_INFO_BUFFER 结构的Offset字段中指定的偏移处 。相应的PAC_INFO_BUFFER的ulType字段设置为 0x0000000D
class PAC_DEVICE_INFO(NDRSTRUCT):
# 是 PAC 的可变长度缓冲区,应该包含DC提供的设备的登录和授权信息。指向PAC_DEVICE_INFO结构的指针被序列化为一个字节数组,并直接放置在最顶层PACTYPE 结构的缓冲区数组之后,位于缓冲区中相应PAC_INFO_BUFFER 结构的偏移字段中指定的偏移处。相应的PAC_INFO_BUFFER的ulType字段设置为 0x0000000E。
class PAC_DEVICE_CLAIMS_INFO(Structure):
# PAC 的可变长度缓冲区,应该包含客户端的编组声明blob。PAC_DEVICE_CLAIMS_INFO 结构直接放置在最顶层 PACTYPE 结构的 Buffers 数组之后 ,位于Buffers数组中相应PAC_INFO_BUFFER 结构的Offset字段中指定的偏移处 。相应的PAC_INFO_BUFFER的ulType字段设置为 0x0000000F
class VALIDATION_INFO(TypeSerialization1):
在/examples/getPac.py中用于解析接收到的pac数据
class S4U2SELF:
def printPac(self, data):
# ① 解开票据 enc-part,经 AD_IF_RELEVANT 取出 PAC
encTicketPart = decoder.decode(data, asn1Spec=EncTicketPart())[0]
adIfRelevant = decoder.decode(encTicketPart['authorization-data'][0]['ad-data'], asn1Spec=AD_IF_RELEVANT())[
0]
# So here we have the PAC
pacType = PACTYPE(adIfRelevant[0]['ad-data'].asOctets())
buff = pacType['Buffers']
# ② 遍历 PAC_INFO_BUFFER,按 ulType 分发解析
for bufferN in range(pacType['cBuffers']):
infoBuffer = PAC_INFO_BUFFER(buff)
data = pacType['Buffers'][infoBuffer['Offset']-8:][:infoBuffer['cbBufferSize']]
if logging.getLogger().level == logging.DEBUG:
print("TYPE 0x%x" % infoBuffer['ulType'])
if infoBuffer['ulType'] == 1:
# ③ 登录信息(KERB_VALIDATION_INFO):跳过 4 字节指针 ReferentID 后解析 NDR 结构
type1 = TypeSerialization1(data)
newdata = data[len(type1)+4:]
kerbdata = KERB_VALIDATION_INFO()
kerbdata.fromString(newdata)
kerbdata.fromStringReferents(newdata[len(kerbdata.getData()):])
kerbdata.dump()
print('Domain SID:', kerbdata['LogonDomainId'].formatCanonical())
# 其余类型(CLIENT_INFO / SERVER_CHECKSUM / PRIVSVR_CHECKSUM / UPN_DNS_INFO)
# 仅在 DEBUG 模式 dump,结构与上面同构,略
elif infoBuffer['ulType'] == PAC_CLIENT_INFO_TYPE:
......
elif infoBuffer['ulType'] == PAC_SERVER_CHECKSUM:
......
elif infoBuffer['ulType'] == PAC_PRIVSVR_CHECKSUM:
......
elif infoBuffer['ulType'] == PAC_UPN_DNS_INFO:
......
else:
hexdump(data)
buff = buff[len(infoBuffer):]