32blogby StudioMitsu

How Are Passwords Actually Cracked? Attack Methods & Modern Defenses

Learn how brute force, dictionary attacks, and rainbow tables work with code examples. Why MD5/SHA-1 fails in the GPU era, how to use Argon2id/bcrypt correctly, and what the 2025 NIST update changed.

14 min read
On this page

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.

LengthNumbers OnlyLowercaseLower + UpperLower + Upper + NumbersFull ASCII
6InstantInstantInstantInstant4 seconds
8Instant22 hours8 months3 years12 years
102 hours15 years41,000 years345,000 years37 million years
129 days10,000 years1 billion years19 billion years2 trillion years
142 years262 million years3 trillion years1 quadrillion years
16202 years7 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.

python
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.

python
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:

python
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.

python
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.

python
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):

AlgorithmSpeed (hashes/sec)Time to Crack 8-char Alphanumeric
MD5164,100 MH/s~22 minutes
SHA-150,600 MH/s~72 minutes
SHA-25622,000 MH/s~2.8 hours
bcrypt (32 iterations)184 kH/sCenturies

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.

python
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.

javascript
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

FeatureArgon2idbcryptscryptPBKDF2
Year2015199920092000
Memory-hardYes (tunable)No (4 KB fixed)Yes (tunable)No
GPU-resistantStrongModerateStrongWeak
Side-channel resistantYes (id variant)N/ANoN/A
Max password lengthUnlimited72 bytesUnlimitedUnlimited
OWASP rank1st3rd2nd4th

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.

RuleOld (Rev 3)New (SP 800-63B Rev 4, 2025)
Minimum length8 characters15 characters (no MFA) / 8 (with MFA)
Complexity requirementsRequired (upper, lower, number, symbol)Abolished
Periodic rotationEvery 60-90 daysAbolished (change only on compromise)
Leaked password checkOptionalRequired
Maximum lengthOften 16-20At least 64 characters must be supported
Passkeys / FIDO2Not addressedOfficially recommended
SMS-based 2FAAllowedRestricted (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:

  1. Your client hashes the password with SHA-1
  2. It sends only the first 5 characters of the hash to the HIBP API
  3. The API returns all known hashes that start with those 5 characters (typically 400-800 results)
  4. 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.

bash
# 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.