Password Cracking and Wordlist Engineering Part 1: The Hash Landscape and Why Offline Cracking Is a Different Game
Lab-only note up front, same as the series this one continues from: everything below runs against a documented, simulated lab — the CONTOSO.LOCAL domain built for the Impacket Deep Dive series. Running any of this against a network you don’t own or aren’t explicitly authorised to test is a computer crime in most jurisdictions. The defensive half of every post here is the actual point, not a disclaimer bolted on to justify the offensive half.
You’ve already seen these hashes
Part 3 of the Impacket series pulled this off svc-legacy-scan:
$krb5asrep$23$svc-legacy-scan@CONTOSO.LOCAL:a1f3c9e0b7d24e118f... (truncated)
Part 4 pulled this off svc-backup:
$krb5tgs$23$*svc-backup$CONTOSO.LOCAL$MSSQLSvc/FS01.CONTOSO.LOCAL~1433*$8a3f... (truncated, saved to spns.txt)
Both posts stated the cracked plaintexts — Sc4nn3r2019! and B4ckup$SQL2024 — and moved on, because those posts were about getting the hash, not breaking it. That’s this series’ entire subject: what happens between “I have a blob of ciphertext” and “I have svc-backup’s actual password,” and why that process looks nothing like guessing a login form.
Online guessing vs. offline cracking: not the same problem
An online attack — hammering a login form, an SSH daemon, an RDP endpoint — is bounded by the defender. Account lockout policies, rate limiting, fail2ban, MFA: all of these work because the attacker has to ask the server whether each guess is right, and the server can simply stop answering, or start logging very loudly, or lock the account after five tries.
Offline cracking has none of that. Once an attacker has the hash — via secretsdump.py’s DCSync in Part 7, or a Kerberoasted ticket, or a stolen /etc/shadow — every subsequent guess is a local computation against data the attacker already possesses. No server to ask, no lockout to trip, no logs to generate, no rate limit but the hardware’s own throughput. The only thing standing between the hash and the plaintext is compute time and the quality of the guesses being tried — which is exactly what the rest of this series is about: hashcat and John the Ripper supply the compute, wordlists and rules supply the guesses.
That reframes the entire defensive question. Password policy that only thinks about online attacks — lockout thresholds, CAPTCHA, MFA — does nothing once a hash has left the building. The real defense against offline cracking is making the hash itself expensive to test, or making sure it never leaves in a crackable form to begin with. Both of those threads run through this series and land fully in Part 8.
The hash landscape this series covers
| Format | hashcat mode | Where it comes from | Speed class |
|---|---|---|---|
| NTLM | 1000 | secretsdump.py (SAM, NTDS.dit), pass-the-hash material | Fast — unsalted MD4 |
| NetNTLMv2 | 5600 | Captured network auth (Responder, relay logs) | Fast-ish — HMAC-MD5, salted by challenge |
| Kerberoast (TGS-REP, etype 23) | 13100 | GetUserSPNs.py (Part 4) | Fast — RC4-HMAC |
| AS-REP (etype 23) | 18200 | GetNPUsers.py (Part 3) | Fast — RC4-HMAC |
bcrypt $2*$ | 3200 | Modern web app password stores | Slow — tunable cost factor |
sha512crypt $6$ | 1800 | /etc/shadow on most Linux distros | Slow-ish — 5,000 rounds by default |
That “speed class” column is the single most important thing in this table, and it’s worth being precise about why. NTLM is MD4(UTF-16LE(password)) — one round of an unsalted, deliberately fast hash function designed in 1990 for checksums, not password storage. Kerberoasting and AS-REP hashes are RC4-HMAC constructions built on top of the same NTLM key — fast for the same reason. bcrypt and sha512crypt exist specifically to not be fast: they’re built around a tunable cost factor (bcrypt’s $2b$12$... — that 12 is the log2 round count) that a defender can dial up as hardware gets faster. Verified directly against the installed hashcat v6.2.6 binary’s --help output, these are the exact mode numbers this series uses throughout — not copied from a five-year-old cheat sheet:
$ hashcat --help | grep -iE "13100|18200|1000 |3200|NTLM|Kerberos"
1000 | NTLM | Operating System
5600 | NetNTLMv2 | Network Protocol
13100 | Kerberos 5, etype 23, TGS-REP | Network Protocol
18200 | Kerberos 5, etype 23, AS-REP | Network Protocol
3200 | bcrypt $2*$, Blowfish (Unix) | Operating System
Recognizing a hash before you crack it
Every format above has a distinctive shape, and getting this wrong wastes real time — pointing hashcat at the wrong mode just returns “no hashes loaded” or, worse, silently fails to match anything without explaining why. The formats that matter for this series are easy to eyeball once you know the tells:
$krb5tgs$23$*user$REALM$spn*$...— Kerberoasting output, exactly whatGetUserSPNs.py -format hashcatwrites. The23is the Kerberos encryption type (etype 23 = RC4-HMAC, the one that matters here — etypes 17/18 are AES and use different hashcat modes, 19700/19800).$krb5asrep$23$user@REALM:...— AS-REP roasting output fromGetNPUsers.py -format hashcat. Same etype convention as above.aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::— a SAM/NTDS line fromsecretsdump.py,username:RID:LMHash:NTHash:::. That specific LM hash (aad3b435...) is the fixed value LM produces for an empty or disabled LM field — every modern Windows box shows it, it isn’t a real credential. The NT hash (31d6cfe0...) is the one that matters, and in this specific case it’s also a well-known constant: it’sMD4(UTF-16LE("")), the NTLM hash of an empty password — Administrator and Guest both showed exactly this value in Part 7’s dump, meaning both accounts are disabled-with-blank-password rather than crackable. I confirmed that independently rather than assuming it from memory:
$ python3 -c "
from Crypto.Hash import MD4
h = MD4.new(); h.update(''.encode('utf-16le'))
print(h.hexdigest())
"
31d6cfe0d16ae931b73c59d7e0c089c0
Matches exactly. That single verification is a useful habit in itself: an NT hash of 31d6cfe0d16ae931b73c59d7e0c089c0 never needs a wordlist — it’s recognizable on sight, and secretsdump output that contains it is telling you an account is disabled, not that it’s weakly protected.
Where this series is going
- The hash landscape (this post) — what’s crackable, what isn’t, and why offline cracking changes the threat model entirely.
- Hashcat fundamentals — attack modes, real mode numbers, and the GPU economics behind why some formats above are a coffee break and others are a wall.
- John the Ripper — core vs. Jumbo (a distinction that turned out to matter more than expected once actually installed and tested), and when JtR earns a place next to hashcat rather than being redundant with it.
- Building wordlists that actually work — rockyou and its limits,
cewl,crunch, and OSINT-driven, target-specific lists. - The rule engine — hashcat’s mutation rules,
best64.rule, and writing custom ones instead of only downloading them. - Mask and hybrid attacks — keyspace math,
.hcmaskfiles, and when brute force is actually the rational choice. - Cracking CONTOSO.LOCAL end to end — every technique in this series run against the actual hashes the Impacket series produced.
- The defensive mirror — passphrase design, bcrypt/scrypt/Argon2 cost economics against real GPU numbers, and a critique of rotation-policy advice that doesn’t hold up.
Next up: Part 2, where the hash landscape above gets a compute budget attached to it — real, sourced hashcat throughput numbers per format, and what that throughput actually costs to rent.