Application Security
password cracking
hashcat
bcrypt
argon2
+3 more

How Hackers Crack Passwords: Hashcat, Rainbow Tables & Why bcrypt Isn't Enough

SCRs Team
March 19, 2026
15 min read
Share

The Reality of Password Cracking in 2026

An NVIDIA RTX 5090 can compute 164 billion MD5 hashes per second. That means every possible 8-character password using letters, numbers, and symbols can be cracked in under 3 hours.

Cracking Speed by Hash Algorithm (RTX 5090)

AlgorithmSpeedTime for 8-char password
MD5164B hashes/sec~2.5 hours
SHA-152B hashes/sec~8 hours
SHA-25622B hashes/sec~19 hours
NTLM (Windows)280B hashes/sec~1.5 hours
bcrypt (cost 12)32K hashes/sec~2,800 years
Argon2id8K hashes/sec~11,200 years

Key insight: The hash algorithm matters more than password complexity. MD5 with a 20-character password is still weaker than bcrypt with a 10-character password.


Attack Type 1: Dictionary Attack

The simplest and most effective attack. Uses wordlists of common passwords, leaked credentials, and language dictionaries.

# Hashcat dictionary attack
hashcat -m 0 -a 0 hashes.txt rockyou.txt

# -m 0 = MD5
# -a 0 = Dictionary attack
# rockyou.txt = 14 million leaked passwords

Top 10 wordlists:

  1. RockYou — 14M passwords from 2009 breach
  2. SecLists — Curated collection by Daniel Miessler
  3. CrackStation — 1.5B words from all major breaches
  4. Have I Been Pwned passwords — 900M+ unique passwords
  5. Weakpass — Multi-terabyte wordlists

Attack Type 2: Rule-Based Mutations

Applies transformations to dictionary words — capitalizing, appending numbers, leetspeak substitutions.

# Hashcat with rules
hashcat -m 0 -a 0 hashes.txt rockyou.txt -r rules/best64.rule

# Common rule transformations:
# password → Password (capitalize first)
# password → password123 (append numbers)
# password → p@ssw0rd (leetspeak)
# password → drowssap (reverse)
# password → Password! (capitalize + append symbol)
# password → PASSWORD (uppercase all)

Popular rule files:

  • best64.rule — 64 most effective rules
  • d3ad0ne.rule — 35K rules, very thorough
  • OneRuleToRuleThemAll — 52K expertly curated rules

Attack Type 3: Mask/Brute Force

When you know the password pattern:

# Try all 8-character combinations: uppercase + lowercase + digits
hashcat -m 0 -a 3 hashes.txt ?a?a?a?a?a?a?a?a

# Masks:
# ?l = lowercase  ?u = uppercase  ?d = digit  ?s = symbol
# ?a = all characters

# Common patterns people use:
hashcat -m 0 -a 3 hashes.txt ?u?l?l?l?l?l?d?d     # Word + 2 digits
hashcat -m 0 -a 3 hashes.txt ?u?l?l?l?l?l?l?s      # Word + symbol
hashcat -m 0 -a 3 hashes.txt ?d?d?d?d?d?d?d?d       # 8-digit PIN

Attack Type 4: Rainbow Tables

Pre-computed hash → password lookup tables. Instant cracking but requires massive storage.

CharsetLengthTable SizeLookup Time
a-z, 0-91-8460GBInstant
a-z, A-Z, 0-91-71.2TBInstant
All printable1-73TBInstant

Defense against rainbow tables: Salt every hash.

// ❌ No salt — vulnerable to rainbow tables
const hash = crypto.createHash('sha256').update(password).digest('hex');

// ✅ Random salt — rainbow tables are useless
const salt = crypto.randomBytes(16).toString('hex');
const hash = crypto.createHash('sha256').update(salt + password).digest('hex');
// Store both salt and hash

// ✅ Best: bcrypt includes salt automatically
const hash = await bcrypt.hash(password, 12); // Salt is embedded in the hash

Why bcrypt Alone Isn't Enough in 2026

bcrypt's max input is 72 bytes. Longer passwords are silently truncated.

// These produce THE SAME hash:
await bcrypt.hash('A'.repeat(72), 12);
await bcrypt.hash('A'.repeat(72) + 'ANYTHING_ELSE', 12); // Truncated!

Solution: Pre-hash with SHA-256 before bcrypt (or use Argon2id)

// ✅ Pre-hash to handle any length password
import { createHash } from 'crypto';
import bcrypt from 'bcrypt';

function secureHash(password: string): Promise<string> {
  // SHA-256 the password first (produces 64 hex chars, well under 72 bytes)
  const preHash = createHash('sha256').update(password).digest('hex');
  return bcrypt.hash(preHash, 12);
}

// ✅ Even better: Use Argon2id (no length limit, memory-hard)
import argon2 from 'argon2';

const hash = await argon2.hash(password, {
  type: argon2.argon2id,
  memoryCost: 65536,  // 64MB
  timeCost: 3,
  parallelism: 4,
});

What Actually Makes Passwords Secure

Forget complexity rules. Length and randomness are what matter:

PasswordEntropyTime to Crack (bcrypt cost 12)
P@ssw0rd~30 bitsIn dictionary, instant
correct horse battery staple~44 bits~1 year
kj#8Fm2p$Lq9~78 bitsEffectively never
5 random words (diceware)~65 bits~10,000 years

Modern Password Policy Recommendations (NIST 800-63B)

  • ✅ Minimum 8 characters (12+ recommended)
  • ✅ Check against breached password lists (HaveIBeenPwned API)
  • ✅ Allow all characters including spaces and Unicode
  • No forced complexity rules (uppercase + number + symbol)
  • No forced rotation (unless breach suspected)
  • No security questions (easily researched)
// Modern password validation
import { pwnedPassword } from 'hibp';

async function validatePassword(password: string): Promise<string[]> {
  const errors: string[] = [];
  
  if (password.length < 12) errors.push('Password must be at least 12 characters');
  if (password.length > 128) errors.push('Password must be under 128 characters');
  
  // Check breached passwords
  const breachCount = await pwnedPassword(password);
  if (breachCount > 0) {
    errors.push(\`This password appeared in ${breachCount} data breaches. Choose a different one.\`);
  }
  
  return errors;
}

The most important takeaway: Use Argon2id, enforce minimum length, check breached lists, and stop requiring special characters. That's it.

Advertisement