ApriorIT

The Kerberos protocol is a significant improvement over previous authentication technologies. It uses strong cryptography and third-party ticket authorization to minimize the risk of cybersecurity incidents.

In this article, we share useful tips on performing Kerberos authentication using the Local Security Authority (LSA) API based on our own experience. We also introduce our KerberosSkeleton open-source project that can be used as a template for your future projects.

This article will be useful for developers who want to implement Kerberos authentication for Windows servers without using the Security Support Provider Interface (SSPI). It’s written for programmers who already have basic knowledge of the Kerberos protocol and know what Kerberos authentication is.

Contents:

What is Kerberos and why do we need it?

Implementing Kerberos authentication with the KerberosSkeleton open-source project

Obtaining TGT and TGS tickets

   Retrieving the SPN from a server

   Retrieving TGT and TGS tickets for authentication

Working with the server library

Conclusion

What is Kerberos and why do we need it?

Kerberos is a network authentication protocol created at the Massachusetts Institute of Technology (MIT). The main advantage of this protocol is that it doesn’t transmit unencrypted tickets across the network during authentication, thus eliminating the possibility of password interception.

The Kerberos protocol can be used in various situations, including when:

  • implementing Single Sign-On (SSO) technology
  • establishing encrypted connections between servers and clients
  • authenticating users when logging on to a Windows system
  • authenticating with web services

In this article, we’re not going to focus on the Kerberos protocol, as we assume you’re already familiar with the ways it works. You can find more basic information about this protocol in the Windows Dev Center.

The basic working principle of the Kerberos protocol is the following:

  1. The client requests and receives a ticket-granting ticket (TGT) from the key distribution center (KDC).
  2. The client requests and receives a ticket-granting service (TGS) ticket from the KDC.
  3. The client sends the TGS ticket to the server they want to authenticate on.
  4. Using the TGT and TGS tickets, the server authenticates the client, creates a new logon session, and receives a user token handle.
  5. Using this user token handle, the client can impersonate tokens to gain access to certain server resources.
  6. Once the authentication process is over, the client and the server will have a shared session key that can be used to sign messages and establish cryptographic connections.
how kerberos works

Related services

Security Testing

For programmers working with Kerberos authentication, Microsoft has created a Security Support Provider Interface (SSPI) API.

However, if you don’t want to use the SSPI API, there’s an alternative solution. You can implement Kerberos authentication using an API for interacting with the Local Security Authority (LSA) service — a system service for creating a logon session on a local computer. This method can be used, for instance, with Credential Provider features for logging in to the system without entering a password.

As with the SSPI API, with the LSA API the Kerberos protocol is mostly implemented inside the API functions:

  • Establishing connections to the KDC
  • Sending messages to the KDC
  • Receiving messages from the KDC
  • Authenticating users using TGT and TGS tickets
  • And so on

At the same time, you can interact with the LSA service only through the API provided by the system. Direct interactions with the LSA service are impossible.

Since we don’t have the source code for the LSA service, how it interacts with the KDC remains unclear. However, after examining the LSA API, we can assume it should look somewhat like this:

lsa api service

Let’s take a closer look at implementing Kerberos authentication for Windows using the KerberosSkeleton open-source project.

Related services

Web Application Development Services & Solutions

Implementing Kerberos authentication with the KerberosSkeleton open-source project

To illustrate how to perform Kerberos authentication using the LSA API, our team has created the KerberosSkeleton open-source project and placed all the code and class diagrams on GitHub.

Primary objectives of the KerberosSkeleton project:

  • Demonstrate the code in this article
  • Show how to use Kerberos authentication when debugging
  • Prepare for future projects related to Kerberos authentication

The KerberosSkeleton project is available under the MIT license and can be freely used in commercial and open-source projects — for example, as a template for your own project.

To learn Kerberos authentication through debugging with the KerberosSkeleton project, use a PC running Visual Studio (VS) 2015, as the project is implemented on it.

If you want to perform real Kerberos authentication — not unit tests — make sure you have access to a Windows PC and to a Windows Server PC. These systems must belong to the same domain.

To test out Kerberos authentication with the help of KerberosSkeleton, follow these steps:

1. Open the KerberosSkeleton.sln file in VS 2015.

2. Build two projects: ServerApp and ClientApp. You should get two .exe files: ServerApp.exe and ClientApp.exe.

3. Copy the ServerApp.exe file to a Windows Server system (for example, Windows Server 2016).

4. Open the command-line interface on the Windows Server system and run ServerApp.exe with the -spn option. It should look like this:

ServerApp.exe -spn

5. Copy the output. This string should have the following format:

host / <dnsHostName>.<dnsDomainName> @ <dnsDomainName>

6. Copy the ClientApp.exe file to a Windows client — for example, to a Windows 10 PC — that belongs to the same domain as the Windows Server.

7. Open the command-line interface on the Windows client and run ClientApp.exe with two parameters:

  • -spn
  • the output from ServerApp.exe

It should look like this:

ClientApp.exe -spn host/	<dnsHostName>.	<dnsDomainName>@	<dnsDomainName>

8. After that, you should see two .bin files next to ClientApp.exe in the same folder:

  • krbtgtTicket.bin, which is a TGT
  • serviceTicket.bin, which is a TGS ticket

9. Copy these tickets to the Windows Server system and place them next to ServerApp.exe (in the same folder).

10.Open the command-line interface on the Windows Server system and run ServerApp.exe with the -auth parameter:

ServerApp.exe -auth

11. If the tickets are valid, you'll receive a grammatically dubious message saying “Success completed task of the authentication.”

12. If the tickets aren’t valid, you’ll receive a message saying “Failed task of the authentication.”

The Kerberos project also contains unit tests for learning how to use the LSA API for Kerberos authentication in debug mode.

Note that we used mock objects in the KerberosSkeleton project instead of implementing functionality for:

  • sending messages to the client/server
  • receiving messages from the client/server
  • parsing messages from the client/server
  • packing messages into requests/responses

In the next section, we discuss how to obtain TGT and TGS tickets and show what the class diagram in the client library of the KerberosSkeleton project looks like.

Read also:
Authentication As a Service: Architecture, Technologies, and Solutions

Obtaining TGT and TGS tickets

Now, let’s explore how to receive TGT and TGS tickets and see how this process looks in the ClientLib class diagram.

To obtain a TGT, you don’t need any additional information other than you already have, since this ticket is intended specifically for the client. But to get a TGS ticket, you need to transfer the Service Principal Name (SPN) to the KDC. The SPN tells the KDC which service you want to authenticate with. In response, the KDC will make a ticket that can only be read by this service.

Apart from KDC requests, a client has to send two requests to the server to perform Kerberos authentication. The first request is required to receive the SPN. The second is needed for authentication. In order to wrap these two queries in one, choose the Decorator pattern.

The class diagram for the client library of the KerberosSkeleton project looks like this:

client library of kerberosskeleton project

Next, we’ll learn how to retrieve the SPN from a server.

Read also:
Testing SSO Solutions That Use SAML 2.0 and OAuth 2.0 in Windows Active Directory

Retrieving the SPN from a server

After receiving a client request to retrieve an SPN response, a server forms the response and returns it to the client. The SPN response format must fit the format used in Active Directory for registering the service. In the KerberosSkeleton project, the SPN response format looks like this:

host/	<dnsHostName>.	<dnsDomainName>@	<dnsDomainName>

Obtaining the SPN on the client side of the KerberosSkeleton project consists of the following steps:

  1. Create an object that implements the IServerRequest interface needed for sending an SPN request to the server.
  2. Create a KerbAuthRequestDecorator object. Its constructor is needed for passing the object created in the previous step.
  3. Call the SendRequest method from the KerbAuthRequestDecorator object. To send the SPN request, this object calls the SendRequest method of the spnRequest object. A message for receiving the SPN response is sent in the body of the method. Once the the response is received, an object of the class IServerResponse is created. This object contains the SPN.
  4. The KerbAuthRequestDecorator object analyzes the result. The SPN has to match the spn format. Otherwise, an exception terminates the authentication process with an error.
  5. If the SPN matches the spn format, the KerbAuthRequestDecorator object generates an authentication request.

Let’s see how all these steps look in code. The example below shows the code in which we create an object of the KerbAuthRequestDecorator class. The SPN request is passed in the constructor.

TEST(KerbAuthRequestDecorator, success)
{
    int functionToFailing = 0;
    Secur32::Secur32WrapperPtr secur32Wrapper(new MockSecur32Wrapper(functionToFailing));
    ServerRequestPtr spnRequest(new MockServerRequest(Mock::RequestType::SPN_Request));
    SpnValidatorPtr spnValidator(new SpnValidator());
    ServerResponsePtr authResponse;
    KerbAuthRequestDecoratorPtr kerbAuthenticator;
    ASSERT_NO_THROW(kerbAuthenticator.reset(new KerbAuthRequestDecorator(std::move(spnRequest),
                                                                     std::move(secur32Wrapper),
                                                                     std::move(spnValidator))));
    ASSERT_NO_THROW(authResponse = kerbAuthenticator->SendRequest());
    ASSERT_EQ(authResponse->GetStringDataFromResponse(L"auth"), L"Some authentication response");
}

In this example:

1. ServerRequestPtr is the type specified in the IServerRequest.h file.

typedef std::unique_ptr<iserverrequest> ServerRequestPtr;

2. SpnValidatorPtr is the type determined in the ISpnValidator.h file (the SPN validator interface).

typedef std::unique_ptr	<ISpnValidator> SpnValidatorPtr;

3. Secur32::Secur32WrapperPtr is the type set in ISecur32Wrapper.h. The main goal of this class is to provide access to the system API for interacting with the LSA service.

typedef std::unique_ptr	<ISecur32Wrapper> Secur32WrapperPtr;

The spnRequest and secur32Wrapper objects are implemented in a mock object, since we didn’t implement the client–server connection features in the KerberosSkeleton project.

The code below shows what happens in the SendRequest method of the AKerbAuthRequestDecorator class, which is a base class for KerbAuthRequestDecorator.

ServerResponsePtr AKerbAuthRequestDecorator::SendRequest()
{
    IServerRequest* spnRequest = GetRequest();
    ServerResponsePtr spnResponse;
    TicketData serviceTicket;
    TicketData krbtgtTicket;
    const ULONG ticketFlags = GetKerberosTicketFlags();
    static const std::wstring krbtgtName = L"krbtgt";
    static const std::wstring SPN_KEY = L"SPN";
    spnResponse = spnRequest->SendRequest();
    const std::wstring servicePrincipalName = spnResponse->GetStringDataFromResponse(SPN_KEY);
    bool isSpnDataValid = m_spnValidator->Validate(servicePrincipalName);
    if (!isSpnDataValid)
    {
        throw KerberosException(
            KerberosException::ErrorType::INVALID_SPN_DATA,
            "AKerbAuthRequestDecorator::SendRequest: spn data is invalid"
        );
    }
    …
}

Let’s explore what’s shown in the code in detail:

1. Get a pointer to the SPN request.

2. Create a smart pointer to IServerResponse (ServerResponsePtr). This smart pointer will take over the object after calling the spnRequest->SendRequest function.

3. Create two empty std::vector<unsigned char> objects. TicketData is a typedef for the std::vector object. TicketData objects will be filled out later.

4. Create the ticketFlags variable and assign it the number 0x60A00000, which is a result of calling the GetKerberosTicketFlags function. Here’s an example of this function:

ULONG AKerbAuthRequestDecorator::GetKerberosTicketFlags()const
{
    return KerberosTicketOptions::Forwardable | KerberosTicketOptions::Forwarded |
           KerberosTicketOptions::Renewable | KerberosTicketOptions::Pre_authent;
}

For more details about the types of flags and their meanings, explore the Microsoft documentation: Event 4768(S, F): A Kerberos authentication ticket (TGT) was requested. The ticketFlags variable will be used later after receiving the tickets.

5. Create two strings: krbtgtName and SPN_KEY.

  • krbtgtName is the name of the ticket that must be used when receiving a TGT.
  • SPN_KEY is the key that helps you to get an SPN from the response.

6. Use a pointer to SPN Request to send a request to the server. The server processes this request, generates an SPN, and responds to the client. The client receives the response from the server and records the received data to the spnReponse variable.

7. To get the SPN as a string, call the GetStringDataFromResponse method with SPN_KEY as a function parameter to get the SPN from the response, not something else.

8. After receiving SPN as a string, validate it for compliance with a specific format. To do this, call the Validate method on the m_spnValidator variable, passing the SPN received from the server. Use regular expressions — std::regex and std::cmatch — for SPN validation.

9. In the end, check the result of validations. If the validation is successful, move on to authentication. Otherwise, throw an exception, which will terminate the authentication process.

Once you receive the SPN string, you have all the information you need to get a TGS ticket. TGT and TGS tickets will later be packed in an authentication request to the server.

Related services

Kernel and Driver Development

Retrieving TGT and TGS tickets for authentication

To receive TGT and TGS tickets, perform the following steps:

  1. Establish a connection to the LSA service and receive the LSA HANDLE data type.
  2. Get a unique identifier for the authentication package (used when requesting tickets).
  3. Fill out the KERB_RETRIEVE_TKT_REQUEST structure to obtain a TGT.
  4. Get a TGT by parsing KERB_RETRIEVE_TKT_RESPONSE. Free up memory occupied by KERB_RETRIEVE_TKT_RESPONSE.
  5. Fill out the KERB_RETRIEVE_TKT_REQUEST structure to receive a TGS ticket.
  6. Get a TGS ticket by parsing KERB_RETRIEVE_TKT_RESPONSE. Free up memory occupied by KERB_RETRIEVE_TKT_RESPONSE.
  7. Close the LSA service connection by closing the LSA HANDLE.

We implemented the KerberosTicketsManger class to receive Kerberos tickets in the KerberosSkeleton project. The main method of this class — RequestTicketFromSystem — accepts two parameters:

  1. std::vector, in which the received ticket will be written
  2. a string in which the target name of the ticket is indicated; the value of this string indicates which ticket you will receive

In our case:

  • To get a TGT, use the krbtgt string as a second parameter.
  • To receive a TGS ticket, use the SPN string received earlier as the second parameter.

Let’s explore how it will look in code:

ServerResponsePtr AKerbAuthRequestDecorator::SendRequest()
{
    IServerRequest* spnRequest = GetRequest();
    ServerResponsePtr spnResponse;
    TicketData serviceTicket;
    TicketData krbtgtTicket;
    const ULONG ticketFlags = GetKerberosTicketFlags();
    static const std::wstring krbtgtName = L"krbtgt";
    static const std::wstring SPN_KEY = L"SPN";
    spnResponse = spnRequest->SendRequest();
    const std::wstring servicePrincipalName = spnResponse->GetStringDataFromResponse(SPN_KEY);
    bool isSpnDataValid = m_spnValidator->Validate(servicePrincipalName);
    if (!isSpnDataValid)
    {
        throw KerberosException(
            KerberosException::ErrorType::INVALID_SPN_DATA,
            "AKerbAuthRequestDecorator::SendRequest: spn data is invalid"
        );
    }
    typedef std::unique_ptr	<kerberosticketsmanger> KerberosTicketsMangerPtr;
    const KerberosTicketsMangerPtr kerbTicketsManger(new KerberosTicketsManger(m_secur32Wrapper));
    kerbTicketsManger->SetTicketFlags(ticketFlags);
    kerbTicketsManger->RequestTicketFromSystem(serviceTicket, servicePrincipalName);
    kerbTicketsManger->SetCacheOptions(KERB_RETRIEVE_TICKET_AS_KERB_CRED);
    kerbTicketsManger->RequestTicketFromSystem(krbtgtTicket, krbtgtName);
    const ServerRequestPtr authRequest = PackTicketsToRequest(serviceTicket, krbtgtTicket);
    ServerResponsePtr authResponse = authRequest->SendRequest();
    return authResponse;
} 

The first thing to do is create a KerberosTicketsManger object.

  1. Call SetTicketFlags.
  2. Get the TGS ticket by calling the RequestTicketFromSystem method, passing a vector that contains the result of calling RequestTicketFromSystem and the SPN string.
  3. Set a new value for cacheOptions by calling SetCacheOptions.
  4. Receive a TGT by calling the RequestTicketFromSystem method, passing a vector that contains the result of calling RequestTicketFromSystem and the krbtgt string.

Although the Kerberos documentation tells us to get a TGT first and only then get a TGS ticket, we did the opposite. In fact, when receiving a TGS ticket in the API functions, several operations take place:

  1. Getting a TGT and recording it to the cache
  2. Getting a TGS ticket by using the TGT from the cache
  3. Recording the received TGS ticket to the out parameter of the API function

Thus, when receiving a TGS ticket, you already have a TGT in the cache. Therefore, when there’s a second request for a TGT, the ticket is not requested from the KDC but is taken from the cache.

Let’s go back to the SendRequest function and take a closer look at it.

When you create a KerberosTicketsManger object, a connection is established with the LSA service in the object’s constructor:

void KerberosTicketsManger::InitializeUntrustedConnect()
{
    HANDLE hLsa = NULL;
    NTSTATUS ticketsStatus = m_secur32Wrapper->LsaConnectUntrusted(&hLsa);
    if (STATUS_SUCCESS != ticketsStatus)
    {
        throw KerberosException(
            KerberosException::ErrorType::FAILED_LSACONNECTUNTRUSTED,
            "KerberosTicketsManger::InitializeConnection: LsaConnectUntrusted failed"
        );
    }
    m_hLsa = LsaHandlePtr(hLsa, GetLsaHandleDeleter());
    LSA_STRING lsaStrAuthPkg = {};
    lsaStrAuthPkg.Length = static_cast	<ushort>(strlen(MICROSOFT_KERBEROS_NAME_A));
    lsaStrAuthPkg.MaximumLength = static_cast	<ushort>(strlen(MICROSOFT_KERBEROS_NAME_A));
    lsaStrAuthPkg.Buffer = MICROSOFT_KERBEROS_NAME_A;
    ticketsStatus = m_secur32Wrapper->LsaLookupAuthenticationPackage(m_hLsa.get(),
                                                                     &lsaStrAuthPkg,
                                                                     &m_authPkgId);
    if (STATUS_SUCCESS != ticketsStatus)
    {
        throw KerberosException(
            KerberosException::ErrorType::FAILED_LSALOOKUPAUTHENTICATIONPACKAGE,
            "KerberosTicketsManger::InitializeConnection: LsaLookupAuthenticationPackage failed"
        );
    }
}

Let’s explore how the connection is established step by step:

  1. The LsaConnectUntrusted function establishes a connection to the LSA service. If everything is done correctly, you’ll receive LSA HANDLE.
  2. Create a smart pointer for LSA HANDLE, which is a field of the m_hLsa class. In the KerberosTicketsManger destructor, when destroying the m_hLsa object, the LsaDeregisterLogonProcess method will be called.
  3. Create the LSA_STRING structure, where the MICROSOFT_KERBEROS_NAME_A macro is equal to the Kerberos string.
  4. Call the LsaLookupAuthenticationPackage function. If the function call is successful, at the output you’ll get a unique identifier of the authentication package — this is a field of the m_authPkgId class.

Next, you need to call two methods: SetTicketFlags and RequestTicketFromSystem. There’s no need for exploring the SetTicketFlags function in detail, as nothing complicated happens there. Only the class field will change.

Let’s take a look at the RequestTicketFromSystem method in detail:

void KerberosTicketsManger::RequestTicketFromSystem(TicketData& vecTicket,
                                                    const std::wstring& tgtName)const
{
    KerbRetrieveTktRequest kerbRetrieveTktRequest(tgtName);
    ULONG responseLen = static_cast	<ulong>(-1);
    NTSTATUS protocolStatus = STATUS_ACCESS_DENIED;
    KERB_RETRIEVE_TKT_RESPONSE* pResp = NULL;
    kerbRetrieveTktRequest.SetTicketFlags(m_ticketFlags);
    kerbRetrieveTktRequest.SetCacheOptions(m_cacheOptions);
    NTSTATUS ticketsStatus = m_secur32Wrapper->LsaCallAuthenticationPackage(
        m_hLsa.get(),
        m_authPkgId,
        reinterpret_cast<pvoid>(kerbRetrieveTktRequest.GetRetrieveTktRequest()),
        kerbRetrieveTktRequest.Length(),
        reinterpret_cast<PVOID*>(&pResp),
        &responseLen,
        &protocolStatus
    );
    typedef std::unique_ptr<KERB_RETRIEVE_TKT_RESPONSE, LsaBufferDeleter> LsaBufferDeleterPtr;
    const LsaBufferDeleterPtr ticketPtr(pResp, GetLsaBufferDeleter());
    if (STATUS_SUCCESS != ticketsStatus)
    {
        throw KerberosException(
              KerberosException::ErrorType::FAILED_LSACALLAUTHENTICATIONPACKAGE,
              "KerberosTicketsManger::RequestTicketFromSystem: LsaCallAuthenticationPackage
               failed");
    }
    if (STATUS_SUCCESS != protocolStatus)
    {
        throw KerberosException(
              KerberosException::ErrorType::INVALID_PROTOCOLSTATUS,
              "KerberosTicketsManger::RequestTicketFromSystem: ProtocolStatus failed");
    }
    if (pResp == NULL)
    {
        throw KerberosException(
              KerberosException::ErrorType::INVALID_RETURNBUFFER,
              "KerberosTicketsManger::RequestTicketFromSystem: Buffer equal NULL in response");
    }
    UCHAR* pEncodedTicketBeg = ticketPtr->Ticket.EncodedTicket;
    UCHAR* pEncodedTicketEnd = ticketPtr->Ticket.EncodedTicket + ticketPtr->Ticket.EncodedTicketSize;
    vecTicket.clear();
    vecTicket.assign(pEncodedTicketBeg, pEncodedTicketEnd);
}

If the LsaCallAuthenticationPackage function is called successfully, you’ll get a pointer to the KERB_RETRIEVE_TKT_RESPONSE structure. Then, using the Ticket.EncodedTicket field of this structure, you can get the data for the requested ticket.

Let’s explore the parameters passed to the LsaCallAuthenticationPackage function:

1. You can get the LSA HANDLE - m_hLsa.get() parameter when calling the LsaConnectUntrusted function.

2. The m_authPkgId parameter is the unique identifier of the authentication package. You get it when calling the LsaLookupAuthenticationPackage API function.

3. Then you have a pointer to the KERB_RETRIEVE_TKT_REQUEST structure. To fill out this structure, you should use such values as the Kerberos ticket name (the krbtgt string or spn string), ticketFlags, and cachedOptions. Note: the data in the output structure (KERB_RETRIEVE_TKT_RESPONSE.Ticket.EncodedTicket) depends on the data we include to the KERB_RETRIEVE_TKT_REQUEST structure.

4. The next parameter is the size of the KERB_RETRIEVE_TKT_REQUEST structure.

5. The fifth parameter is a pointer to the KERB_RETRIEVE_TKT_RESPONSE structure pointer. When the LsaCallAuthenticationPackage function is executed successfully, memory will be allocated for the KERB_RETRIEVE_TKT_RESPONSE structure and it will be filled out with the corresponding data. Free up the memory occupied by this structure by calling the LsaFreeReturnBuffer API function. To call the LsaFreeReturnBuffer method, create a smart pointer to the KERB_RETRIEVE_TKT_RESPONSE structure pointer.

6. The next parameter, responseLen, is a pointer to the unsigned long variable, in which the size of the received KERB_RETRIEVE_TKT_RESPONSE structure will be recorded.

7. The last parameter is NTSTATUS, which indicates the completion status of the authentication packet.

Let’s take a closer look at the creation and filling of the KERB_RETRIEVE_TKT_REQUEST structure.

To create this structure and fill it out, use the implemented KerbRetrieveTktRequest class, which is a wrapper for the KERB_RETRIEVE_TKT_REQUEST structure. The main method of this class is the GetRetrieveTktRequest method, which returns a pointer to the KERB_RETRIEVE_TKT_REQUEST structure.

Here’s the code of the GetRetrieveTktRequest function:

KERB_RETRIEVE_TKT_REQUEST* KerbRetrieveTktRequest::GetRetrieveTktRequest()
{
    KERB_RETRIEVE_TKT_REQUEST* ret = NULL;
    ret = reinterpret_cast<KERB_RETRIEVE_TKT_REQUEST*>(&m_retrieveTktRequestData[0]);
    LUID luidLogonId;
    memset(&luidLogonId, 0, sizeof(LUID));
    SecHandle hSec;
    memset(&hSec, 0, sizeof(SecHandle));
    ret->MessageType = KERB_PROTOCOL_MESSAGE_TYPE::KerbRetrieveEncodedTicketMessage;
    ret->LogonId = luidLogonId;
    ret->TargetName.Length = static_cast<ushort>(m_targetName.length() * sizeof(wchar_t));
    ret->TargetName.MaximumLength = static_cast<ushort>(m_targetName.length() * sizeof(wchar_t));
    ret->TargetName.Buffer = (wchar_t*)(ret + 1);
    memcpy(ret->TargetName.Buffer, m_targetName.c_str(), ret->TargetName.Length);
    ret->TicketFlags = m_ticketFlags;
    ret->CacheOptions = m_cacheOptions;
    ret->EncryptionType = 0;
    ret->CredentialsHandle = hSec;
    return ret;
}

Once TGT and TGS tickets are created, they’re packed into an authentication request and this request is sent to the server. The operations to be performed on the server side are discussed in the following section.

Read also:
Sandbox-Evading Malware: Techniques, Principles, and Examples

Working with the server library

There are two operations that need to be performed on the server side:

  1. When receiving a request for an SPN: create an SPN and send it to the client
  2. When receiving an authentication request: pull both the TGT and TGS tickets from the request, process authentication using both TGT and TGS tickets, and send authentication result to the client.

To organize the process of handling client requests, we used the Strategy pattern. This is what the server library of the KerberosSkeleton project looks like:

server library of kerberosskeleton project

Note that in the ASpnResponseStrategy object, when receiving a string in the format host/<DnsHostname>.<DnsDomain>@<DnsDomain>, only the GetComputerNameExW system API function is used to receive the DnsHostname and DnsDomain parameters.

Let’s explore how the KerbAuthStrategy method works in detail. Say the server detects an authentication request, pulls TGT and TGS tickets, and creates a class object inherited from the AkerbAuthStrategy object. Let’s try to understand the logic that detects the request type and creates an appropriate action strategy by taking a look at the unit test:

TEST(TestKerbAuthStrategy, success)
{
    TicketData serviceTicket = CreateKerbTicketFromString("Some server ticket");
    TicketData krbtgtTicket = CreateKerbTicketFromString("Some krbtgt ticket");
    ClientResponsePtr response;
    KerbAuthStrategyPtr kerbAuthStrategy(new KerbAuthStrategy(serviceTicket, krbtgtTicket, 0));
    ASSERT_NO_THROW(response = kerbAuthStrategy->CreateResponse());
    const std::string& authenticationResult = kerbAuthStrategy->GetAuthenticationResult();
    ASSERT_EQ(authenticationResult, "success authentication");
}

Everything seems pretty simple: the KerbAuthStrategy class object is created and then its CreateResponse method is called. This is what the CreateResponse method looks like:

ClientResponsePtr AKerbAuthStrategy::CreateResponse()
{
    ClientResponsePtr response;
    try
    {
        KerbAuthenticatorPtr kerbAuthenticator = GetKerbAuthenticator();
        m_hToken = TokenHandlePtr(kerbAuthenticator->Authenticate(GetServiceTicket(),
                                                                  GetKrbtgtTicket()), &::CloseHandle);
        response = PackSpnToResponse("success authentication");
    }
    catch (const KerbException::KerberosException& /*ex*/)
    {
        //TO DO: write to the log file here.
        response = PackSpnToResponse("failed authentication");
        m_hToken.reset(nullptr);
    }
    return response;
}

Here, an object of the KerbAuthenticator class is created. A connection with the LSA service is established in the constructor of this class the same way as in the client.

After connecting to the LSA service, LSA HANDLE (m_hLsa) and the unique authentication packet identifier (m_authPkgId) are also written in the fields of the KerbAuthenticator class.

Then the Authenticate method of the KerbAuthenticator object is called. This method accepts TGT and TGS tickets as parameters.

Let’s take a closer look at the Authenticate method:

HANDLE AKerbAuthenticator::Authenticate(const TicketData& serviceTicket, const TicketData& krbtgtTicket)
{
    const LsaLogonUserDataManagerPtr lsaLogonUserDataManager(new LsaLogonUserDataManager(
                                                                                GetSystemApiWrapper(),
                                                                                GetOriginName(), 
                                                                                GetSourceModuleIdentifier()));
    ULONG kerbTicketLogonLen = 0;
    KERB_TICKET_LOGON * pKerbTicketLogon = lsaLogonUserDataManager->GetKerbTicketLogon(serviceTicket,
                                                                                       krbtgtTicket,
                                                                                       kerbTicketLogonLen);
    LSA_STRING lsastrOriginName = {};
    lsaLogonUserDataManager->InitLsaOrigin(lsastrOriginName);
    TOKEN_SOURCE sourceContext = {};
    lsaLogonUserDataManager->InitTokenSource(sourceContext);
    PLUID logonId = lsaLogonUserDataManager->GetDefaultLogonId();
    PQUOTA_LIMITS quotaLimits = lsaLogonUserDataManager->GetDefaultQuotaLimits();
    PKERB_TICKET_PROFILE profileBuffer = NULL;
    HANDLE hToken = nullptr;
    ULONG profileBufferLen = -1;
    NTSTATUS subStatus = -1;
    NTSTATUS status = m_secur32Wrapper->LsaLogonUser(m_hLsa.get(),
                                                     &lsastrOriginName,
                                                     GetLogonType(),
                                                     m_authPkgId,
                                                     pKerbTicketLogon,
                                                     kerbTicketLogonLen,
                                                     NULL, // additional local groups
                                                     &sourceContext,
                                                     reinterpret_cast<PVOID*>(&profileBuffer),
                                                     &profileBufferLen,
                                                     logonId,
                                                     &hToken,
                                                     quotaLimits,
                                                     &subStatus);
    typedef std::unique_ptr<KERB_TICKET_PROFILE, LsaBufferDeleter> LsaProfileBufferPtr;
    const LsaProfileBufferPtr profileBufferPtr(profileBuffer, GetLsaBufferDeleter());
    if (status != STATUS_SUCCESS)
    {
        throw KerberosException(
            KerberosException::ErrorType::FAILED_LSALOGONUSER,
            "AKerbAuthenticator::Authenticate: LsaLogonUser failed");
    }
    return hToken;
}

The LsaLogonUser system function accepts many parameters. Let’s explore their meanings and origins:

1. Get the LSA HANDLE - m_hLsa.get() parameter when calling the LsaConnectUntrusted function.

2. lsastrOriginName is a parameter that identifies who is trying to log in. The origin name can vary. In the KerberosSkeleton project, the origin name is an exmln string.

3. The SECURITY_LOGON_TYPE enum sets the type for a logon request. In the KerberosSkeleton project, this value is SECURITY_LOGON_TYPE::Network.

4. The m_authPkgId parameter passes the unique identifier of the authentication packet. To get this parameter, call the LsaLookupAuthenticationPackage function.

5. The pointer to the KERB_TICKET_LOGON structure is called pKerbTicketLogon. For creating and filling this structure, the LsaLogonUserDataManager class was created. To get a pointer to the KERB_TICKET_LOGON structure, call GetKerbTicketLogon from the LsaLogonUserDataManager class. Here’s what the GetKerbTicketLogon function code looks like:

KERB_TICKET_LOGON* LsaLogonUserDataManager::GetKerbTicketLogon(
                                                     const TicketData& serviceTicket,
                                                     const TicketData& krbtgtTicket,
                                                     ULONG& length)
{
    length = sizeof(KERB_TICKET_LOGON) + 
             static_cast<ulong>(serviceTicket.size()) +
             static_cast<ulong>(krbtgtTicket.size());
    m_kerbTicketLogonData.resize(length, 0);
    KERB_TICKET_LOGON* pKerbTicketLogon = NULL;
    pKerbTicketLogon = reinterpret_cast<KERB_TICKET_LOGON*>(&m_kerbTicketLogonData[0]);
    pKerbTicketLogon->MessageType = KerbTicketLogon;
    pKerbTicketLogon->Flags = m_kerbTicketLogonFlag;
    pKerbTicketLogon->ServiceTicketLength = static_cast<ulong>(serviceTicket.size());
    pKerbTicketLogon->TicketGrantingTicketLength = static_cast<ulong>(krbtgtTicket.size());
    pKerbTicketLogon->ServiceTicket = reinterpret_cast<puchar>(pKerbTicketLogon + 1);
    memcpy(pKerbTicketLogon->ServiceTicket, &serviceTicket[0], serviceTicket.size());
    pKerbTicketLogon->TicketGrantingTicket = pKerbTicketLogon->ServiceTicket + serviceTicket.size();
    memcpy(pKerbTicketLogon->TicketGrantingTicket, &krbtgtTicket[0], krbtgtTicket.size());
    return pKerbTicketLogon;
}

As you can see from the code, TGT and TGS tickets are copied to this structure, and the corresponding pointers are written to the ServiceTicket and TicketGrantingTicket fields.

6. You can see the size of the KERB_TICKET_LOGON structure, kerbTicketLogonLen.

7. The next parameter is a pointer to TOKEN_GROUPS. In this parameter, it’s required to transmit additional group identifiers. In the KerberosSkeleton project, this parameter is equal to NULL.

8. The sourceContext parameter is a pointer to the TOKEN_SOURCE structure and identifies the source module. To create it, you need a string that identifies the source module. This string, just like originName, can vary. In the KerberosSkeleton project, it’s looks like this: m_srcModuleIdentifies = “exm”. Here is the code that fills TOKEN_SOURCE in the KerberosSkeleton project:

void LsaLogonUserDataManager::InitTokenSource(TOKEN_SOURCE& srcContext)const
{
    memset(&srcContext, 0, sizeof(TOKEN_SOURCE));
    const char* tmp = m_srcModuleIdentifies.c_str();
    strncpy_s(srcContext.SourceName,
              TOKEN_SOURCE_LENGTH,
              m_srcModuleIdentifies.c_str(),
              TOKEN_SOURCE_LENGTH - 1);
    if (!m_systemWrapper->AllocateLocallyUniqueId(&srcContext.SourceIdentifier))
    {
        DWORD error = ::GetLastError();
        // TO DO: Write to logs the error code;
        throw KerbServerException(
                     KerbServerException::FAILED_INIT_TOKENSOURCE,
                     "LsaLogonUserDataManager::LsaLogonUserDataManager: Failed on allocation the
                      local unique Id");
    }
}

Note that the size of the m_srcModuleIdentifies string shouldn’t be more than 8 bytes. Also, note that AllocateLocallyUniqueId is a system API.

9. The profileBuffer parameter is a pointer to the KERB_TICKET_PROFILE structure. This is an out parameter that results from the execution of the LsaLogonUser function. When exiting the Authenticate function, you need to free the memory occupied by this structure by creating a profileBufferPtr smart pointer. In the profileBufferPtr destructor, the LsaFreeReturnBuffer system API will be called.

10. Then we pass a pointer to an unsigned long, in which the size of the KERB_TICKET_PROFILE structure, profileBufferLen, will be written.

11. The logonId parameter is a pointer to the LUID structure. This is an out parameter, which contains the unique identifier for the logon session.

12. The hToken parameter is also an out parameter and a pointer to HANDLE. This parameter records the new user token created for this session. You’ll have to return this parameter from the Authenticate function. Note that after you use HANDLE and no longer need it, you should call the CloseHandle API function to eliminate leaks. In the KerberosSkeleton project, this is achieved by creating a smart pointer to HANDLE, in the destructor of which the CloseHandle API function is called.

13. The quotaLimits parameter is also an out parameter, which will be filled out once the hToken parameter is equal to the main token. In this case, process quota limits will be situated in this parameter.

14. The last parameter is NTSTATUS. This parameter is an additional result of executing the LsaLogonUser function— subStatus. This value is set only if the user account information is valid but the logon request is rejected. At the same time, this parameter will contain additional information indicating the reason for failure. This parameter can take one of the following values:

  • STATUS_INVALID_LOGON_HOURS
  • STATUS_INVALID_WORKSTATION
  • STATUS_PASSWORD_EXPIRED
  • STATUS_ACCOUNT_DISABLED

In our description of work with the server library, we have shown a class diagram for the server library of the KerberosSkeleton project, explored how the KerbAuthStrategy and Authenticate methods work, and taken a closer look at the parameters of the LsaLogonUser system function.

Conclusion

In this article, we have explored what to pay attention to when using Kerberos authentication and how to perform authentication with the KerberosSkeleton project. We focused on an alternative method of authentication with the LSA service API and without using the SSPI API.

We hope the open-source KerberosSkeleton project will help you better understand this alternative way of implementing Windows Kerberos authentication and will become a template for your future project. You can also check out the UML architecture of the KerberosSkeleton project.

At Apriorit, we take each project as a mission. Contact us if you’re ready to develop a robust IT solution.

 

Let's talk

4000 chars left
Attach a file
Browse
By clicking Send you give consent to processing your data

Book an Exploratory Call

Do not have any specific task for us in mind but our skills seem interesting? Get a quick Apriorit intro to better understand our team capabilities.

Book time slot

Contact Us

P: +1 202-780-9339
E: [email protected]

8 The Green, Suite #7106, Dover, DE 19901
United States

D-U-N-S number: 117063762

btnUp