Skip to content
pwnsy
passwordsintermediate#passwords#cracking#hashing#authentication#hashcat#john-the-ripper#brute-force#credential-stuffing

How Passwords Get Cracked

A technical breakdown of password storage, cracking techniques, and the math behind why most passwords fail, plus what actually works for defense.

Most people have no idea their password was cracked 36 hours after the breach notification email was sent. The company's database hit a hacking forum, someone spun up a cloud GPU cluster, ran rockyou.txt with a good rule set, and recovered 60% of the hashes before breakfast. This is not a theoretical threat. It happened to LinkedIn in 2012, to Adobe in 2013, to RockYou in 2009. It is happening right now to databases that haven't been disclosed yet.

Understanding how password cracking works — mechanically, mathematically, and operationally — is the only way to make informed decisions about password policies, hashing algorithms, and authentication architecture.

Find your tool

Looking for the right hash cracker, wordlist, or auditing tool? Browse 3,700+ vetted OSINT & security tools at tools.pwnsy.com.

How Passwords Are Actually Stored

Modern applications store a hash of your password, not the plaintext. A cryptographic hash function takes arbitrary input and produces a fixed-length digest. The same input always produces the same output, but you cannot reverse the output to recover the input — at least, not by design.

SHA-256("hunter2")  → f52fbd32b2b3b86ff88ef6c490628285f482af15ddcb29541f94bcf526a3f6c7
SHA-256("hunter2")  → f52fbd32b2b3b86ff88ef6c490628285f482af15ddcb29541f94bcf526a3f6c7
SHA-256("hunter3")  → 02c6e4f2e6f8e6d8e7c9a7f6b4e5d3c2a1b9f8e7d6c5b4a3928374651908172

The problem with raw hashing is determinism. If two users share the same password, their hashes are identical. An attacker who sees a thousand identical hashes in a dump immediately knows a thousand users share that password — and cracking one reveals all of them.

Salting defeats this by prepending or appending a unique random value to each password before hashing:

user_1: salt = "a3f8b2c1"  →  hash = SHA256("a3f8b2c1" + "hunter2")
user_2: salt = "9d4e7f2a"  →  hash = SHA256("9d4e7f2a" + "hunter2")

Both users have the same password, but completely different hashes. Salts are stored alongside the hash in the database — they are not secret; their purpose is to force per-user computation, not to add secrecy.

The Hashing Algorithm Gap

Not all hash functions are appropriate for passwords. Hash functions designed for data integrity (MD5, SHA-1, SHA-256, SHA-3) are optimized to be fast. That optimization is catastrophic for password storage.

A single Nvidia RTX 4090 GPU can compute:

AlgorithmHashes/second
MD5~68 billion
SHA-1~23 billion
SHA-256~11 billion
bcrypt (cost 12)~184,000
Argon2id (64MB/3 iter)~6,500
scrypt (N=32768)~50,000

That is not a typo. A single consumer GPU computes 68 billion MD5 hashes per second. Against bcrypt at cost 12, the same hardware manages 184,000. The design goal of password hashing functions is to be deliberately, tunably slow and memory-intensive — so that offline cracking attacks are economically unviable.

Warning

MD5 and SHA-1 are cryptographically broken for password storage. Not because of collision resistance issues — those are real but separate — but because they are so fast that even a well-salted MD5 database is crackable at scale. If you see either in a codebase as the password storage mechanism, classify it as a critical security vulnerability. The Adobe breach of 2013 exposed 153 million accounts stored with 3DES and hints stored in plaintext. The entire thing was cracked in days.

The LinkedIn Breach: A Concrete Example

In June 2012, LinkedIn suffered a breach that exposed 6.5 million SHA-1 password hashes. They were unsalted. Within hours, security researchers and crackers had identified millions of matches. By 2016, it emerged the actual breach was 117 million accounts. The lack of salting meant that once any password was cracked, all users with the same password were exposed simultaneously. Common passwords like "linkedin", "123456", and "password" matched millions of hashes instantly with a single computation.

The lesson is not that SHA-1 was used — it is that unsalted fast hashes are a catastrophic failure mode that transforms a database breach into mass credential exposure measured in hours, not years.

Cracking Techniques: How Attackers Actually Do It

Brute force enumerates every possible combination of characters up to a specified length. It is guaranteed to crack any password eventually — the question is whether "eventually" is minutes or geological epochs.

The search space grows exponentially with length:

CharsetSizeLengthCombinationsTime @ 184k bcrypt/s
lowercase (a-z)266308,915,776~28 minutes
lowercase + digits3662,176,782,336~3.3 hours
lowercase + digits3682.8 trillion~179 days
Full ASCII printable9586.6 quadrillion~1,147 years
Full ASCII printable95125.4 × 10²³~10¹² years

Against MD5 (68 billion hashes/sec), those numbers shrink by a factor of 370,000. An 8-character lowercase + digit MD5 hash cracks in under a second. An 8-character full-ASCII MD5 hash cracks in roughly 27 hours with one GPU. A cluster of 8 RTX 4090s reduces that to 3 hours.

Hashcat handles brute force with mask attacks. The character sets use shorthand notation:

# Hashcat mask attack notation
# ?l = lowercase (a-z)
# ?u = uppercase (A-Z)
# ?d = digits (0-9)
# ?s = special characters (!@#$...)
# ?a = all printable ASCII
 
# Brute force MD5 hashes (mode 0), 8-character lowercase+digit
hashcat -m 0 -a 3 hashes.txt ?l?l?l?l?l?l?d?d
 
# Brute force bcrypt (mode 3200), 6-character all-printable
hashcat -m 3200 -a 3 hashes.txt ?a?a?a?a?a?a
 
# Custom charset: digits only (PINs)
hashcat -m 0 -a 3 hashes.txt -1 ?d ?1?1?1?1?1?1
 
# Session management (resume later)
hashcat -m 1800 -a 3 shadow.txt ?a?a?a?a?a?a?a --session=bruteforce1
hashcat --session=bruteforce1 --restore
 
# Show cracked results
hashcat -m 1800 shadow.txt --show

The critical insight: brute force is the attacker's fallback, not the primary weapon. When dictionaries and rules fail, brute force fills the gaps — but only for shorter passwords. Anything above 10 characters of reasonable complexity is practically immune to brute force against bcrypt.

Dictionary Attacks: Exploiting Human Predictability

Humans are terrible at choosing random passwords. They choose words, names, dates, sports teams, and keyboard patterns. Dictionary attacks exploit this by hashing a wordlist and comparing against target hashes.

The foundational wordlist is rockyou.txt: 14.3 million passwords recovered from the 2009 RockYou breach, where passwords were stored in plaintext. It remains the most effective starting wordlist in 2026 because it reflects actual human password selection behavior.

# John the Ripper — auto-detect hash type, rockyou dictionary
john --wordlist=/usr/share/wordlists/rockyou.txt hashes.txt
 
# Hashcat — NTLM hashes (Windows), rockyou
hashcat -m 1000 -a 0 ntlm_hashes.txt rockyou.txt
 
# Hashcat — Linux SHA-512crypt from /etc/shadow
hashcat -m 1800 -a 0 shadow.txt rockyou.txt
 
# Hashcat — bcrypt from web app database dump
hashcat -m 3200 -a 0 bcrypt_hashes.txt rockyou.txt

Rule-based mutations are where dictionary attacks become devastating. Rules transform dictionary words to match common human patterns:

password     →  Password, P@ssword, passw0rd, P@ssw0rd, P@ssw0rd1, password123
linkedin     →  LinkedIn, L1nked1n, LinkedIn!, LINKEDIN
baseball     →  Baseball1!, b@seball, Baseball2023

The best64.rule file ships with Hashcat and applies 64 common transformations. The dive.rule file applies thousands. A 14 million word dictionary with dive generates billions of candidates:

# Apply best64 rules to rockyou
hashcat -m 1000 -a 0 hashes.txt rockyou.txt -r /usr/share/hashcat/rules/best64.rule
 
# Stack multiple rule files
hashcat -m 0 -a 0 hashes.txt rockyou.txt \
  -r /usr/share/hashcat/rules/best64.rule \
  -r /usr/share/hashcat/rules/toggles1.rule
 
# Use dive.rule for maximum coverage (slower)
hashcat -m 1000 -a 0 hashes.txt rockyou.txt -r /usr/share/hashcat/rules/dive.rule
 
# John with built-in rules
john --wordlist=rockyou.txt --rules=KoreLogic hashes.txt

Real rule files look like this (Hashcat rule syntax):

# Rules that cover ~80% of human password mutations
:            # Do nothing (keep base word)
l            # Lowercase all letters
u            # Uppercase all letters
c            # Capitalize first letter
$1           # Append "1"
$!           # Append "!"
$2$0$2$3     # Append "2023"
sa@          # Replace 'a' with '@'
se3          # Replace 'e' with '3'
si!          # Replace 'i' with '!'
so0          # Replace 'o' with '0'
c$1          # Capitalize + append "1"
c$1$!        # Capitalize + append "1!"

The KoreLogic ruleset, released publicly from DEFCON challenges, remains one of the best for real-world password cracking. It is available in John the Ripper distributions and documents the specific patterns that appear most frequently in cracked corporate passwords.

Combinator Attacks: Building Compound Passwords

Combinator attacks take two wordlists and concatenate every pair, generating compound passwords like soccer123 or springtime!:

# Hashcat combinator attack (mode 1)
hashcat -m 0 -a 1 hashes.txt wordlist1.txt wordlist2.txt
 
# With rules applied to the left word
hashcat -m 0 -a 1 hashes.txt wordlist1.txt wordlist2.txt -j c
 
# With rules applied to the right word
hashcat -m 0 -a 1 hashes.txt wordlist1.txt wordlist2.txt -k $1$!

This is effective for passwords like summer2023, Dog!horse, or January2025! — patterns where two components (season/color/sport + year/special) are concatenated. These feel complex to users but are straightforward for combinator attacks.

Rainbow Tables: Precomputed Hash Lookups

A rainbow table is a precomputed data structure that maps hash values back to their plaintext inputs. The naive approach would be a simple lookup table: store every (plaintext, hash) pair. For MD5 and an 8-character lowercase alphabet, that's 26^8 × 2 entries — approximately 1.3 TB of storage. Rainbow tables use chains of alternating hash and reduction functions to achieve similar coverage with dramatically less storage.

The math:

  • 8-character lowercase MD5 space: ~208 billion hashes
  • A full rainbow table for this space: ~20–100 GB depending on compression
  • Lookup time for a hit: milliseconds
  • Crack rate for unsalted MD5 in this space: effectively 100%

Tools and services:

# Ophcrack uses rainbow tables for Windows NTLM/LM hashes
ophcrack -t /path/to/tables -f hash.txt
 
# Online: CrackStation, CMD5, OnlineHashCrack — submit hash, get plaintext
# These maintain multi-TB rainbow table databases

Why salting completely defeats rainbow tables: To use a rainbow table against salted hashes, you need a separate table per unique salt. With 16-byte random salts (2^128 possible values), the required storage is physically impossible. Salting is not a minor improvement — it eliminates the entire rainbow table attack class.

Tip

Salting doesn't slow down the hash computation at all. Its only purpose is to invalidate precomputed tables. This is why "we use salted MD5" is not a satisfactory answer to a security audit — the salt defeats rainbow tables, but the speed of MD5 still makes brute force and dictionary attacks fast enough to crack most real-world passwords in hours on commodity hardware.

Credential Stuffing: When You Don't Need to Crack Anything

Credential stuffing is technically not cracking — it is replaying. Billions of username:password pairs from historical breaches are publicly available on hacking forums and distributed freely. Tools automate testing these pairs against live services.

The scale of available credential data as of 2025:

  • Collection #1 (2019): 773 million email/password pairs, 87 GB
  • RockYou2021: 8.4 billion password entries (compilation)
  • COMB (Compilation of Many Breaches): 3.2 billion unique credential pairs
  • Various stealer log aggregations: hundreds of millions of active credentials
# Hydra — HTTP POST form credential stuffing
hydra -C credentials.txt target.com http-post-form \
  "/login:username=^USER^&password=^PASS^:Invalid credentials" \
  -t 64 -w 30
 
# Hydra — against multiple targets
hydra -C credentials.txt -M targets.txt http-post-form \
  "/api/auth:email=^USER^&password=^PASS^:error"
 
# Custom credential stuffing with Python + requests (for proper rate limit handling)
# Real operations use distributed proxies to bypass IP-based rate limits

The effectiveness statistics are sobering. In a 2023 Okta threat report, credential stuffing accounted for 34% of all authentication attacks observed. Success rates vary by service, but even a 0.1% success rate against a 100-million-pair database is 100,000 compromised accounts. The attack requires no hashing, no compute, and runs against live production systems where MFA bypass is a secondary step.

The Okta breach of October 2023 was partly attributed to credential stuffing attacks against the support system using credentials from a prior third-party breach. The attackers accessed files belonging to 134 Okta customers before detection.

PRINCE Attack: Password Probability Chains

The PRINCE (PRobability INfinite Chaining Engine) algorithm generates password candidates by chaining words from a wordlist based on probability distributions derived from cracked passwords. It is more sophisticated than a combinator attack because it models the statistical distribution of real password elements.

# PRINCE via hashcat-utils princeprocessor
./pp64.bin < wordlist.txt | hashcat -m 1000 --stdin hashes.txt
 
# With specific password length targets
./pp64.bin --pw-min=8 --pw-max=12 < words.txt | \
  hashcat -m 1000 --stdin hashes.txt

Hybrid Attacks: Dictionary + Brute Force Combined

Hybrid attacks append or prepend brute-force masks to dictionary words:

# Append 1-4 digit suffix to dictionary words (password → password1234)
hashcat -m 0 -a 6 hashes.txt rockyou.txt ?d?d?d?d
 
# Prepend 2-character mask (20 + password → 20password)
hashcat -m 0 -a 7 hashes.txt ?d?d rockyou.txt
 
# Append year (2020-2029)
hashcat -m 0 -a 6 hashes.txt wordlist.txt 202?d
 
# Append common patterns
hashcat -m 0 -a 6 hashes.txt wordlist.txt ?d?d?d?s

This captures the overwhelming majority of passwords following patterns like BaseWord + Year, BaseWord + !, BaseWord + 123.

Hash Identification and Common Formats

Before cracking, you need to identify the hash type. Unknown hashes can be identified by length, character set, and prefix patterns:

# Hashcat auto-identification
hashcat --identify hash.txt
 
# hash-identifier (standalone tool)
hash-identifier
# Paste hash when prompted
 
# Example hash formats
# MD5:      5f4dcc3b5aa765d61d8327deb882cf99       (32 hex)
# SHA-1:    5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8  (40 hex)
# SHA-256:  6b3a55e0261b...                         (64 hex)
# bcrypt:   $2a$12$R9h/cIPz0gi.URNNX3kh2O...       (60 chars, starts $2)
# Argon2id: $argon2id$v=19$m=65536,t=3,p=4$...
# NTLM:     8846f7eaee8fb117ad06bdd830b7586c       (32 hex, no prefix)
# sha512crypt: $6$rounds=5000$salt$hash...          (starts $6$)

Hashcat mode reference for the most common formats encountered in real dumps:

ModeAlgorithmCommon Source
0MD5Legacy web apps, PHP sites
10md5($pass.$salt)Various custom implementations
20md5($salt.$pass)Various custom implementations
100SHA-1LinkedIn 2012, older web apps
1000NTLMWindows domain hashes
1400SHA-256Moderate security implementations
1700SHA-512Moderate security implementations
1800sha512crypt ($6$)Modern Linux /etc/shadow
3200bcryptWeb apps doing it right
13400KeePass 1.x/2.xPassword manager vault files
16900Ansible VaultDevOps secrets files
22000WPA-PBKDF2-PMKID+EAPOLWiFi handshake captures
22100BitLockerFull disk encryption
13600WinZipEncrypted archives
238001PasswordPassword manager vaults

Password Entropy: The Actual Math

Entropy measures the unpredictability of a password — how much work an attacker must do to guess it by exhaustive search.

Entropy (bits) = log₂(pool_size ^ length) = length × log₂(pool_size)

For a password to be uncrackable with current hardware against bcrypt, target 80+ bits of entropy from a random source. Human-chosen passwords almost never achieve their theoretical entropy.

PasswordPoolLengthTheoretical EntropyPractical Entropy
abc12336631 bits~10 bits (in rockyou)
P@ssw0rd194959 bits~15 bits (in every ruleset)
Tr0ub4dor&3941172 bits~25 bits (XKCD-famous, in wordlists now)
correct-horse-battery2722103 bits~75 bits (diceware, less common)
Random 16-char full ASCII9416105 bits105 bits (truly random)
Random 4-word diceware777651 bits per word selection51 bits (EFF list)
Warning

"P@ssw0rd1" has 59 bits of theoretical entropy but near-zero practical entropy. Every rule file generates it from "password" automatically. Theoretical entropy assumes random, independent selection from the character pool — human cognitive biases mean real passwords inhabit a tiny fraction of their theoretical space. This is why NIST SP 800-63B (2017) dropped complexity requirements in favor of length requirements and breach-database checks.

The NIST SP 800-63B guidance, updated in 2024, now explicitly recommends:

  • Minimum 8 characters (15 recommended)
  • Check against known-breached password lists
  • Do NOT require periodic rotation without evidence of compromise
  • Do NOT require specific character types

Mandatory character types (at least one uppercase, one number, one symbol) produce passwords like Password1! — they meet the rule, they're in every wordlist. Long random passphrases from diceware are more entropy-efficient and more memorable.

Tools in Depth

Hashcat: The GPU Standard

Hashcat is the de facto standard for GPU-accelerated hash cracking. It supports 350+ hash types, scales across multiple GPUs, and is actively maintained.

# System benchmark — shows hash rates for your hardware
hashcat -b
 
# Benchmark specific mode
hashcat -b -m 3200
 
# Identify hash type
hashcat --identify hash.txt
 
# Dictionary attack against NTLM
hashcat -m 1000 -a 0 ntlm.txt rockyou.txt
 
# Dictionary + rules
hashcat -m 1000 -a 0 ntlm.txt rockyou.txt -r rules/best64.rule
 
# Brute force 7-char alphanumeric
hashcat -m 1000 -a 3 ntlm.txt ?a?a?a?a?a?a?a
 
# Combinator
hashcat -m 0 -a 1 hashes.txt dict1.txt dict2.txt
 
# Hybrid: dict + 4-char mask suffix
hashcat -m 0 -a 6 hashes.txt dict.txt ?d?d?d?d
 
# Multi-GPU
hashcat -m 1800 -a 0 shadow.txt rockyou.txt -d 1,2,3
 
# Output cracked passwords to file
hashcat -m 1800 -a 0 shadow.txt rockyou.txt -o cracked.txt --outfmt 2
 
# Restore interrupted session
hashcat --session=myjob --restore
 
# Show already-cracked hashes
hashcat -m 1000 ntlm.txt --show

Performance optimization matters at scale. Hashcat by default uses the OpenCL backend; CUDA often provides better performance on Nvidia hardware:

# Use CUDA backend (Nvidia only)
hashcat -m 1000 ... --backend-devices-virtual 1 -I
 
# Temperature monitoring during long jobs
hashcat -m 1800 ... --gpu-temp-abort=90

John the Ripper: The Flexible Option

John the Ripper (jumbo edition) excels at hash auto-detection and has built-in support for exotic formats that Hashcat doesn't cover. It shines for non-standard hashes and archive cracking.

# Auto-detect hash type and crack
john hashes.txt
 
# Specify wordlist and rules
john --wordlist=/usr/share/wordlists/rockyou.txt --rules=best64 hashes.txt
 
# Specify hash format explicitly
john --format=bcrypt hashes.txt --wordlist=rockyou.txt
 
# Extract and crack Office document passwords
office2john document.docx > office.hash
john office.hash --wordlist=rockyou.txt
 
# Extract and crack ZIP passwords
zip2john protected.zip > zip.hash
john zip.hash --wordlist=rockyou.txt
 
# Crack KeePass database
keepass2john vault.kdbx > keepass.hash
john keepass.hash --wordlist=rockyou.txt
 
# Crack SSH private key passphrase
ssh2john id_rsa > ssh.hash
john ssh.hash --wordlist=rockyou.txt
 
# Show cracked results
john --show hashes.txt
 
# List supported formats
john --list=formats

The john2hashcat utility converts John hash files to Hashcat format, allowing you to switch tools mid-attack.

What Defenders Must Actually Do

On the Application Side: Argon2id or Bust

The OWASP Password Storage Cheat Sheet (2024 update) recommends Argon2id as the first choice. If that's unavailable, scrypt. If neither, bcrypt with a cost factor of 12 or higher. MD5, SHA-1, and SHA-256 are unacceptable regardless of salting.

# Python — argon2-cffi (correct parameterization per OWASP 2024)
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
 
ph = PasswordHasher(
    time_cost=3,        # Number of iterations
    memory_cost=65536,  # 64 MB memory usage
    parallelism=4,      # Parallel threads
    hash_len=32,        # Output hash length in bytes
    salt_len=16         # Salt length in bytes
)
 
# Hash a password
hashed = ph.hash("user_password_here")
# Output: $argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>
 
# Verify (raises VerifyMismatchError on failure)
try:
    ph.verify(hashed, "user_password_here")
    # Returns True if match
except VerifyMismatchError:
    # Password is wrong
    pass
 
# Check if rehash needed (work factor increased since hash was created)
if ph.check_needs_rehash(hashed):
    new_hash = ph.hash("user_password_here")
    # Store new_hash
# Node.js — argon2 package
const argon2 = require('argon2');
 
// Hash
const hash = await argon2.hash(password, {
  type: argon2.argon2id,
  memoryCost: 65536,  // 64 MB
  timeCost: 3,
  parallelism: 4,
});
 
// Verify
try {
  const valid = await argon2.verify(hash, password);
} catch (err) {
  // Failed
}
// Go — golang.org/x/crypto/argon2
package main
 
import (
    "crypto/rand"
    "encoding/base64"
    "golang.org/x/crypto/argon2"
)
 
func HashPassword(password string) string {
    salt := make([]byte, 16)
    rand.Read(salt)
 
    hash := argon2.IDKey([]byte(password), salt, 3, 64*1024, 4, 32)
 
    // Store salt + hash together
    b64Salt := base64.RawStdEncoding.EncodeToString(salt)
    b64Hash := base64.RawStdEncoding.EncodeToString(hash)
    return fmt.Sprintf("$argon2id$v=19$m=65536,t=3,p=4$%s$%s", b64Salt, b64Hash)
}

Work factor calibration: OWASP recommends tuning parameters so that hashing takes 300ms–500ms on your production hardware. This is painful for attackers (300ms per guess on a GPU is crippling) while negligible for users (login delay is imperceptible at under a second).

Reject Known-Breached Passwords

NIST 800-63B explicitly recommends checking new passwords against breach databases. The HaveIBeenPwned API provides this as a privacy-preserving k-Anonymity lookup:

import hashlib
import requests
 
def is_pwned_password(password: str) -> int:
    """
    Returns the count of times this password appears in HIBP breach data.
    Uses k-anonymity: only the first 5 chars of the SHA-1 hash are sent.
    Returns 0 if not found, count if found.
    """
    sha1 = hashlib.sha1(password.encode('utf-8')).hexdigest().upper()
    prefix, suffix = sha1[:5], sha1[5:]
 
    response = requests.get(
        f"https://api.pwnedpasswords.com/range/{prefix}",
        headers={"Add-Padding": "true"}
    )
    response.raise_for_status()
 
    for line in response.text.splitlines():
        hash_suffix, count = line.split(':')
        if hash_suffix == suffix:
            return int(count)
 
    return 0
 
# Usage in registration flow
pwned_count = is_pwned_password(new_password)
if pwned_count > 0:
    raise ValueError(f"This password appears in {pwned_count} breach records. Choose another.")
# API test
curl "https://api.pwnedpasswords.com/range/5BAA6"
# Returns suffixes and counts for hashes starting with 5BAA6
# 1E4C9B93F3F0682250B6CF8331B7EE68FD8:3861493 <- this is "password"

Rate Limiting and Account Lockout

Credential stuffing and online brute force attacks are only viable if the application allows unlimited attempts. Proper rate limiting:

# Redis-backed rate limiter for login endpoints
import redis
from datetime import timedelta
 
r = redis.Redis()
 
def check_login_rate_limit(identifier: str) -> bool:
    """
    identifier: IP address or username
    Returns True if request should be allowed, False if rate limited.
    """
    key = f"login_attempts:{identifier}"
    attempts = r.incr(key)
 
    if attempts == 1:
        r.expire(key, 900)  # 15 minute window
 
    if attempts > 10:
        return False  # Block after 10 attempts in 15 minutes
 
    return True
 
# Progressive lockout: increase delay exponentially
def get_lockout_seconds(username: str) -> int:
    key = f"lockout:{username}"
    count = int(r.get(key) or 0)
    if count == 0:
        return 0
    return min(2 ** count, 3600)  # Cap at 1 hour

MFA: The Actual High-Leverage Control

Microsoft's threat intelligence team published in 2019 that MFA blocks 99.9% of automated account attacks. No password policy achieves that — even the strongest password is phishable. MFA (specifically phishing-resistant MFA: FIDO2/WebAuthn) changes the attack economics entirely.

The practical hierarchy:

  1. Passkeys / FIDO2 hardware keys: Private key never leaves device. Challenge is domain-bound — phishing is mechanically impossible.
  2. TOTP authenticator apps: Eliminates stuffing and most automated attacks. Still vulnerable to real-time phishing proxies (Evilginx).
  3. Push notification MFA: Vulnerable to MFA fatigue attacks (prompt bombing). Require number matching to mitigate.
  4. SMS OTP: SS7-vulnerable, SIM-swap-vulnerable. Better than nothing.
# TOTP generation in Python (for understanding the algorithm)
import hmac, hashlib, struct, time, base64
 
def generate_totp(secret_b32: str, digits: int = 6, period: int = 30) -> str:
    """RFC 6238 TOTP implementation"""
    key = base64.b32decode(secret_b32.upper())
    timestamp = int(time.time()) // period
 
    # Pack timestamp as 8-byte big-endian
    msg = struct.pack('>Q', timestamp)
 
    # HMAC-SHA1
    h = hmac.new(key, msg, hashlib.sha1).digest()
 
    # Dynamic truncation
    offset = h[-1] & 0x0f
    code = struct.unpack('>I', h[offset:offset+4])[0] & 0x7fffffff
 
    return str(code % (10 ** digits)).zfill(digits)

Real-World Cracking Operations

To ground this in practice: here is what a competent attacker does with a fresh NTLM hash dump from a compromised Windows domain:

# 1. Extract hashes from domain (with DA credentials)
# Using impacket secretsdump
secretsdump.py domain/admin:'password'@dc01.corp.local -outputfile dc_hashes
 
# 2. Separate NTLM hashes from the dump
grep ':::' dc_hashes.ntds | cut -d: -f4 > ntlm_only.txt
 
# 3. Quick wins: check against rockyou first
hashcat -m 1000 -a 0 ntlm_only.txt rockyou.txt
 
# 4. Add rules
hashcat -m 1000 -a 0 ntlm_only.txt rockyou.txt \
  -r rules/best64.rule -r rules/toggles1.rule
 
# 5. Combinator attack with common corporate patterns
hashcat -m 1000 -a 1 ntlm_only.txt seasons.txt years.txt
# Generates: Spring2024, Winter2025, Summer2023!, etc.
 
# 6. Hybrid: company name + digits/symbols
echo "CompanyName" > company.txt
hashcat -m 1000 -a 6 ntlm_only.txt company.txt ?d?d?d?d?s
 
# 7. Brute force short passwords (<=8 chars)
hashcat -m 1000 -a 3 ntlm_only.txt ?a?a?a?a?a?a?a?a
 
# 8. Check results
hashcat -m 1000 ntlm_only.txt --show | wc -l

In a typical enterprise domain with 500+ users, a skilled attacker running this workflow against NTLM hashes recovers 40-70% of passwords within a few hours. From those recovered passwords, they identify reuse patterns specific to the organization (e.g., Company2024! is the corporate standard) and apply targeted rules to crack the remainder.

Actionable Defense Checklist

For developers and security engineers:

  • Audit every password storage implementation in your codebase today. Find MD5, SHA-1, SHA-256, SHA-512 raw or salted? Treat as critical.
  • Migrate to Argon2id with OWASP-recommended parameters. Implement rehashing on next login.
  • Integrate HIBP k-anonymity API into your registration and password-change flows.
  • Implement tiered rate limiting: IP-level, username-level, with progressive lockout.
  • Deploy TOTP or WebAuthn MFA. Block SMS OTP for privileged accounts.
  • Enforce minimum password length (15+ chars). Drop complexity rules that produce predictable patterns.
  • Never log passwords. Audit logging code for password fields in request/response logs.
  • Set up alerting for anomalous authentication patterns (velocity, geographic anomaly).

For organizations with Windows environments:

  • Enable Windows Credential Guard to protect NTLM hashes in LSASS memory.
  • Audit for NTLM v1 usage — it is trivially crackable.
  • Block NTLM authentication where Kerberos is available.
  • Enforce MFA for all domain accounts. Azure AD/Entra ID Conditional Access can enforce this at scale.
  • Monitor for secretsdump-style attacks (mass LDAP enumeration, DC replication via DRSUAPI).

For individuals:

  • Use a password manager. Generate 20+ character random passwords for every site.
  • Check your email at haveibeenpwned.com right now.
  • Enable TOTP on your email, banking, and password manager account today.
  • Passkeys wherever supported — Google, Apple, Microsoft, GitHub, and Cloudflare all support them.

The math is not in the attacker's favor against properly implemented bcrypt or Argon2 with strong random passwords. The math is catastrophically against the defender when MD5 meets rockyou with best64 rules. The gap between these two outcomes is a few engineering decisions.

Offline Cracking Versus Online Guessing

The single most important distinction for reasoning about password risk is where the guessing happens. Offline cracking assumes the attacker already holds the hashes, extracted from a stolen database or a dumped /etc/shadow file. There is no server in the loop, no rate limit, no lockout, and no logging. The attacker guesses at the full speed of their hardware, which is why the hash algorithm matters so much. Against offline attacks, the only variables that help you are the slowness of the hashing function and the entropy of the password itself.

Online guessing is the opposite situation. The attacker submits candidate passwords to a live login endpoint and waits for the service to respond. Every guess is subject to whatever rate limiting, lockout, CAPTCHA, and monitoring the service applies. A well defended login endpoint that permits ten guesses per fifteen minutes caps an attacker at roughly a thousand guesses per day per account, which makes even a moderately strong password practically safe from pure online brute force. This is why credential stuffing exists as a category: replaying a password already known from a breach sidesteps the guessing problem entirely, so a single correct attempt succeeds inside the rate limit.

The practical takeaway is that password length and hash choice defend the offline case, while rate limiting, lockout, and MFA defend the online case. A complete authentication design needs both, because the two attack surfaces are independent. A slow hash does nothing to stop credential stuffing, and a strict rate limit does nothing once the database has already leaked.

Detection Signals: Spotting an Attack in Progress

Defenders can observe the online side of these attacks in real time, and the signals are concrete. Credential stuffing produces a characteristic authentication pattern: a high volume of login attempts spread across many distinct usernames, each username tried only once or twice, originating from a large and rotating pool of IP addresses. The failure rate is high because most reused credentials do not match, and the successful logins that do occur come from geographies or device fingerprints that differ from the account's normal history. This maps to MITRE ATT&CK technique T1110.004 (Credential Stuffing) and the broader T1110 Brute Force family.

Password spraying, where an attacker tries a handful of common passwords against a large list of accounts, inverts the ratio: many usernames, very few passwords, deliberately staying under per account lockout thresholds. The detection signal is a low number of failures per account combined with the same small set of passwords appearing across the whole directory in a short window.

Concrete indicators worth alerting on include a sudden spike in the ratio of failed to successful logins, authentication attempts against usernames that do not exist, successful logins immediately preceded by dozens of failures, logins from IP addresses associated with hosting providers or known proxy networks, and impossible travel where one account authenticates from two distant locations within minutes. On the endpoint side, offline cracking of stolen Windows hashes is often preceded by credential dumping activity such as LSASS memory access or volume shadow copy creation to reach the NTDS.dit file, which EDR platforms can flag directly.

Note

A pepper is a secret value added to every password before hashing, stored separately from the database (in application configuration, a hardware security module, or an environment variable rather than the same table as the hashes). If the database leaks but the pepper does not, the stolen hashes are uncrackable without also compromising the application secret. A pepper complements salting: the salt is per user and public, the pepper is global and secret.

Common Misconceptions

Several beliefs about passwords persist despite being wrong, and each one leads to a concrete weakness.

The first is that character complexity rules produce strong passwords. Requiring one uppercase letter, one digit, and one symbol pushes users toward predictable patterns like Password1!, which every rule set generates from the base word automatically. Length from a random source buys far more resistance than mandated character classes.

The second is that salting slows down cracking. Salting forces the attacker to compute each candidate separately per user and defeats precomputed rainbow tables, but it adds no time to a single hash computation. A salted MD5 database still falls to dictionary and brute force attacks at full GPU speed.

The third is that periodic forced rotation improves security. Mandatory ninety day changes drive users to increment a suffix (Spring2025 becomes Spring2026), a pattern that rule based attacks anticipate. NIST SP 800-63B explicitly recommends against scheduled rotation absent evidence of compromise.

The fourth is that MFA makes the password irrelevant. Strong MFA dramatically raises the cost of account takeover, yet real time phishing proxies and MFA fatigue attacks defeat weaker second factors, and a cracked password still exposes any account where MFA was never enabled. The password remains a control worth getting right.

How Password Cracking Evolved

Early cracking was CPU bound and dictionary driven. Tools like the original Crack utility and early John the Ripper iterated wordlists against Unix crypt hashes at a few thousand guesses per second. The economics changed when GPUs became programmable: the massively parallel architecture designed for graphics turned out to be ideal for computing billions of independent hashes at once, and Hashcat built its reputation on exploiting that hardware. The 2009 RockYou breach then handed the community fourteen million real plaintext passwords, which shifted the field from guessing character combinations toward modeling actual human behavior through wordlists and rules.

The defensive response tracked this arms race. Purpose built slow hashes appeared to blunt GPU acceleration: bcrypt in 1999, scrypt in 2009 with deliberate memory hardness to resist GPU and ASIC parallelism, and Argon2 winning the Password Hashing Competition in 2015 as the current recommended standard. Each was designed to make the attacker's hardware advantage as expensive as possible. The live attack surface evolved in parallel, as mass breach compilations made credential stuffing cheaper than cracking, pushing defenders toward breach database checks and phishing resistant MFA as the highest leverage controls.

Frequently asked questions

How long would it take to crack my password? It depends entirely on how it was stored. Against a fast unsalted hash like MD5, a short or common password falls in seconds. Against Argon2id or bcrypt at a proper cost factor, a long random password is infeasible to crack with any current hardware. The password and the hash function together determine the answer, so neither number alone is meaningful.

Is a passphrase really better than a complex password? A random passphrase of four or more words drawn from a large list (diceware style) delivers high entropy while remaining memorable, and it avoids the predictable substitution patterns that rule based attacks target. A short password padded with symbol substitutions offers far less real resistance than its apparent complexity suggests.

Can bcrypt or Argon2 hashes be cracked at all? Yes, but slowly and only for weak inputs. A short or common password stored with bcrypt still falls to a dictionary attack because the attacker only needs to try likely candidates. The slow hash buys time proportional to the strength of the password behind it, so it protects strong passwords well and weak ones poorly.

Does adding one symbol to my password make it much stronger? Marginally, and less than you would expect. Appending a single predictable symbol like ! at the end is one of the first transformations every rule set applies. Genuine strength comes from length and randomness across the whole password, not from a token symbol in a common position.

What is credential stuffing and why can it bypass a strong hash? Credential stuffing replays username and password pairs already exposed in previous breaches against other services. The attacker never touches your hash because they already have a working plaintext password from somewhere you reused it. Unique passwords per site and MFA are the defenses, since hash strength is irrelevant once a valid password is already known.

Should I still change my passwords regularly? Change a password immediately when there is any sign of compromise or when a service you use discloses a breach. Routine scheduled changes without a trigger tend to weaken passwords by pushing users toward predictable increments, which is why modern guidance drops the calendar based requirement.

What is the single most effective thing I can do? Use a password manager to generate a long unique random password for every account, and enable phishing resistant MFA (a passkey or FIDO2 security key) everywhere it is supported. Together these defeat reuse, resist cracking, and neutralize most account takeover attempts.

Sources & further reading