An 8-character lowercase password can be cracked in 22 hours with modern GPUs — and that is against bcrypt, one of the stronger hashing algorithms. Against MD5, it falls in seconds. If you think your password is safe just because it has a few numbers in it, this article will change your mind.
Password cracking is not a mysterious hacker skill. It is math, hardware, and publicly available software. Once you understand how it works, you can defend against it. This article covers the real attack methods, the code behind them, and the modern defenses that actually hold up in 2025.
How Fast Can Your Password Be Cracked
The table below is based on Hive Systems' 2024 password cracking benchmark, which assumes an attacker using 12 x RTX 4090 GPUs against bcrypt hashes (32 iterations, i.e. cost factor 5). With the recommended cost factor of 12, cracking times would be roughly 128x longer. Cracking times against weaker algorithms like MD5 would be orders of magnitude faster.
| Length | Numbers Only | Lowercase | Lower + Upper | Lower + Upper + Numbers | Full ASCII |
|---|---|---|---|---|---|
| 6 | Instant | Instant | Instant | Instant | 4 seconds |
| 8 | Instant | 22 hours | 8 months | 3 years | 12 years |
| 10 | 2 hours | 15 years | 41,000 years | 345,000 years | 37 million years |
| 12 | 9 days | 10,000 years | 1 billion years | 19 billion years | 2 trillion years |
| 14 | 2 years | 262 million years | 3 trillion years | 1 quadrillion years | — |
| 16 | 202 years | 7 trillion years | — | — | — |
The takeaway is clear: length matters far more than complexity. A 12-character lowercase password takes 10,000 years. A 6-character password with every symbol on your keyboard takes 4 seconds.
This is why modern security guidance has shifted from "make it complex" to "make it long." We will come back to this when we cover the 2025 NIST update.
How Passwords Are Stored — What Hashing Actually Means
When you create an account on a well-built site, your password is not stored directly. Instead, the server runs it through a hash function — a one-way mathematical transformation that produces a fixed-length output.
import hashlib
password = "hunter2"
hashed = hashlib.sha256(password.encode()).hexdigest()
print(hashed)
# f52fbd32b2b3b86ff88ef6c490628285f482af15ddcb29541f94bcf526a3f6c7
Key properties of a hash function:
- Deterministic — the same input always produces the same output
- One-way — you cannot reverse the hash to get the original password
- Avalanche effect — changing one character completely changes the output
- Fixed length — whether the input is 3 characters or 3 million, the output length is the same
When you log in, the server hashes what you typed and compares it to the stored hash. If they match, you are in. The server never needs to know your actual password.
This is the theory. In practice, not every service follows it.
In May 2025, a security researcher discovered an unprotected Elasticsearch instance containing 184 million credentials in plaintext — raw email and password pairs for services including Apple, Google, Facebook, and dozens of government portals. No hashing. No encryption. Just a text file anyone could read.
Attack Method 1: Brute Force & Dictionary Attacks
Brute Force
The simplest attack: try every possible combination until you find a match.
import hashlib
import itertools
import string
target_hash = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
charset = string.ascii_lowercase + string.digits
max_length = 6
for length in range(1, max_length + 1):
for attempt in itertools.product(charset, repeat=length):
candidate = "".join(attempt)
candidate_hash = hashlib.sha256(candidate.encode()).hexdigest()
if candidate_hash == target_hash:
print(f"Found: {candidate}")
break
This Python script is intentionally slow — it runs on a CPU, single-threaded. Real attackers use tools like Hashcat that run on GPUs and test billions of hashes per second. But the logic is identical: generate candidates, hash them, compare.
The math works against brute force quickly. For an 8-character password using lowercase + digits (36 characters), there are 36^8 = 2.8 trillion combinations. But for 6 characters, it is only 36^6 = 2.2 billion — a GPU handles that in seconds.
Dictionary Attacks
Instead of trying every combination, dictionary attacks use lists of known passwords. The most famous is RockYou — originally leaked in 2009 from the RockYou gaming site, which stored about 32 million user accounts in plaintext (roughly 14 million unique passwords).
The original RockYou list has been expanded over the years. RockYou2024, published in July 2024, contains approximately 10 billion unique passwords compiled from decades of breaches. Attackers do not need to guess — they just need to check if your password is on the list.
A dictionary attack is simply:
import hashlib
target_hash = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
with open("rockyou.txt", "r", encoding="utf-8", errors="ignore") as f:
for line in f:
candidate = line.strip()
candidate_hash = hashlib.sha256(candidate.encode()).hexdigest()
if candidate_hash == target_hash:
print(f"Found: {candidate}")
break
Credential Stuffing
Credential stuffing is not a cracking technique — it is what happens after cracking succeeds. Attackers take email/password pairs leaked from one breach and try them on other services. Since roughly 65% of people reuse passwords across multiple accounts, this works disturbingly often.
Services like Netflix, Spotify, and gaming platforms are frequent targets. The attacker does not need to crack anything — they just log in with your credentials from a different breach.
Attack Method 2: Rainbow Tables
Brute force and dictionary attacks hash each candidate on the fly. Rainbow tables take a different approach: pre-compute all the hashes in advance and store them in a lookup table.
Instead of calculating SHA-256("password") during the attack, you calculate it once, store the result in a table mapping 5e884898da... back to "password", and then any future attack against SHA-256 is just a database lookup.
import hashlib
# Building a simple rainbow table (conceptual example)
rainbow_table = {}
wordlist = ["password", "123456", "qwerty", "letmein", "admin", "welcome"]
for word in wordlist:
h = hashlib.sha256(word.encode()).hexdigest()
rainbow_table[h] = word
# Attacking with the rainbow table — instant lookup
stolen_hash = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
if stolen_hash in rainbow_table:
print(f"Cracked: {rainbow_table[stolen_hash]}")
else:
print("Not found in table")
Real rainbow tables are enormous. A complete table for 8-character alphanumeric SHA-256 hashes would be several terabytes. But storage is cheap, and the tables only need to be generated once.
The Defense: Salting
A salt is a random value added to the password before hashing. Each user gets a unique salt, which is stored alongside the hash.
import hashlib
import os
def hash_with_salt(password: str) -> tuple[str, str]:
salt = os.urandom(16).hex()
salted = salt + password
hashed = hashlib.sha256(salted.encode()).hexdigest()
return salt, hashed
def verify(password: str, salt: str, stored_hash: str) -> bool:
salted = salt + password
return hashlib.sha256(salted.encode()).hexdigest() == stored_hash
# Two users with the same password get different hashes
salt1, hash1 = hash_with_salt("password")
salt2, hash2 = hash_with_salt("password")
print(f"User 1: {hash1}")
print(f"User 2: {hash2}")
print(f"Same hash? {hash1 == hash2}") # False
Salting defeats rainbow tables because the attacker would need a separate table for every possible salt value. With a 16-byte salt, that is 2^128 possible tables — not happening.
However, salting alone does not slow down brute force. If the underlying hash function is fast (like SHA-256), an attacker can still brute-force salted hashes at high speed. That is where the next section comes in.
Why MD5 and SHA-1 Can't Protect You — The GPU Reality
MD5, SHA-1, and SHA-256 were designed to be fast. That is exactly the problem. They were built for verifying file integrity, not for storing passwords. Speed is a feature for checksums but a vulnerability for password hashing.
Here is what a single NVIDIA RTX 4090 can do with Hashcat (v6.2.6 benchmark):
| Algorithm | Speed (hashes/sec) | Time to Crack 8-char Alphanumeric |
|---|---|---|
| MD5 | 164,100 MH/s | ~22 minutes |
| SHA-1 | 50,600 MH/s | ~72 minutes |
| SHA-256 | 22,000 MH/s | ~2.8 hours |
| bcrypt (32 iterations) | 184 kH/s | Centuries |
Read that table carefully. MD5 runs at 164 billion hashes per second. bcrypt (at 32 iterations, the same setting Hive Systems used) runs at 184,000. That is roughly 890,000 times faster. With the recommended cost factor of 12 (~1,400 H/s), the gap widens to over 100 million times. Argon2id is even slower than bcrypt due to its memory-hard design.
If a database hashed with MD5 gets leaked, every 8-character alphanumeric password is cracked in about 22 minutes on a single GPU — or under 2 minutes with 12 GPUs.
The GPU problem is only getting worse. Each new generation of graphics cards brings more cores and faster memory. Cloud GPU rental services mean an attacker does not even need to own the hardware — they can rent a cluster of A100s for a few dollars per hour.
Modern Defenses — Argon2id vs bcrypt vs scrypt
The solution is to use a hash function that is intentionally slow and memory-hard. These are called key derivation functions (KDFs), and they are specifically designed for password hashing.
OWASP recommends them in this priority order: Argon2id > scrypt > bcrypt > PBKDF2.
Argon2id
Winner of the 2015 Password Hashing Competition. Argon2id combines Argon2i (resistant to side-channel attacks) and Argon2d (resistant to GPU attacks). It lets you tune CPU time, memory usage, and parallelism independently.
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=1, # Number of iterations
memory_cost=47104, # ~46 MB of RAM (OWASP recommendation)
parallelism=1, # Parallel threads
)
# Hash a password
hashed = ph.hash("correct-horse-battery-staple")
print(hashed)
# $argon2id$v=19$m=47104,t=1,p=1$randomsalt$hashoutput
# Verify a password
try:
ph.verify(hashed, "correct-horse-battery-staple")
print("Password is correct")
except Exception:
print("Password is wrong")
bcrypt
The veteran of password hashing, released in 1999 and still holding up. The cost parameter controls how many rounds of hashing are performed — each increment doubles the time.
import bcrypt from "bcrypt";
const saltRounds = 12;
// Hash a password
const hashed = await bcrypt.hash("correct-horse-battery-staple", saltRounds);
console.log(hashed);
// $2b$12$randomsaltandhashoutputhere
// Verify a password
const match = await bcrypt.compare("correct-horse-battery-staple", hashed);
console.log(match); // true
Comparison
| Feature | Argon2id | bcrypt | scrypt | PBKDF2 |
|---|---|---|---|---|
| Year | 2015 | 1999 | 2009 | 2000 |
| Memory-hard | Yes (tunable) | No (4 KB fixed) | Yes (tunable) | No |
| GPU-resistant | Strong | Moderate | Strong | Weak |
| Side-channel resistant | Yes (id variant) | N/A | No | N/A |
| Max password length | Unlimited | 72 bytes | Unlimited | Unlimited |
| OWASP rank | 1st | 3rd | 2nd | 4th |
bcrypt's 72-byte limit means passwords longer than 72 bytes are silently truncated. For most real-world passwords this is not a problem, but it is worth knowing.
The 2025 NIST Update — What Changed
NIST Special Publication 800-63B (Digital Identity Guidelines) was finalized as Revision 4 in July 2025, and the changes are significant. Many of the password rules you grew up with are officially gone.
| Rule | Old (Rev 3) | New (SP 800-63B Rev 4, 2025) |
|---|---|---|
| Minimum length | 8 characters | 15 characters (no MFA) / 8 (with MFA) |
| Complexity requirements | Required (upper, lower, number, symbol) | Abolished |
| Periodic rotation | Every 60-90 days | Abolished (change only on compromise) |
| Leaked password check | Optional | Required |
| Maximum length | Often 16-20 | At least 64 characters must be supported |
| Passkeys / FIDO2 | Not addressed | Officially recommended |
| SMS-based 2FA | Allowed | Restricted (TOTP/FIDO2 preferred) |
The reasoning behind these changes is backed by research:
- Complexity rules lead users to predictable patterns like
P@ssw0rd!— they do not increase entropy meaningfully - Forced rotation causes users to increment numbers (
Password1->Password2) rather than choosing new passwords - Leaked password checks catch the most common attack vector: credential stuffing with known passwords
The shift toward passkeys (FIDO2/WebAuthn) is perhaps the most important change. Passkeys use public-key cryptography — there is no password to crack, no hash to steal, and phishing becomes structurally impossible.
How to Check If Your Password Has Been Leaked
Have I Been Pwned — The k-Anonymity Model
Have I Been Pwned (HIBP) lets you check passwords against a database of over 900 million compromised passwords without ever sending your password to their server.
Here is how it works:
- Your client hashes the password with SHA-1
- It sends only the first 5 characters of the hash to the HIBP API
- The API returns all known hashes that start with those 5 characters (typically 400-800 results)
- Your client checks locally whether the full hash appears in the returned list
This is called k-anonymity — the server never sees enough information to determine your password.
# Check if "password" has been breached
# SHA-1 of "password" = 5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8
# Send first 5 chars (5BAA6) to the API
curl -s https://api.pwnedpasswords.com/range/5BAA6 | head -5
# Output (hash suffix:count):
# 003D68EB55068C33ACE09247EE4C639306B:5
# 1E4C9B93F3F0682250B6CF8331B7EE68FD8:52256179
# ...
# The second line matches: 1E4C9B93F3F... = rest of SHA-1("password")
# Found 52,256,179 times in breaches
You can script this into your own registration flow. If a user tries to sign up with a password found in HIBP, block it and ask them to choose a different one. This is exactly what the 2025 NIST guidelines require.
Password Manager Monitoring
Services like NordPass include a dark web monitoring feature that continuously checks your saved credentials against breach databases. Unlike HIBP, which checks one password at a time, a password manager can scan your entire vault automatically and flag compromised entries.
The real value is not just detection — it is the workflow. When a breach is found, you can generate a new random password and update it directly from the app, rather than manually visiting each compromised site.
Wrapping Up
Password cracking is not magic. It is brute math running on fast hardware. Here is what matters:
- Length beats complexity. A 15-character lowercase passphrase is stronger than an 8-character symbol soup. The 2025 NIST guidelines reflect this.
- Use password-specific hashing. Argon2id is the current best choice. bcrypt is still solid. MD5 and SHA-1 are not password hashing algorithms — stop using them as if they are.
- Salt every hash. Without salting, rainbow tables make cracking trivial. Every modern KDF handles this automatically.
- Check against known breaches. The HIBP API is free and uses k-anonymity. There is no reason not to integrate it.
- Enable passkeys where possible. Public-key authentication eliminates password cracking entirely. It is the long-term solution.
- Never reuse passwords. Credential stuffing is the most common attack. A password manager with a strong master password solves this.
The era of "8 characters with a special symbol" is over. The math has moved on. Make sure your defenses have too.