The Salt That Breaks the Rainbow — Defeating Password Cracking with One Simple Ingredient
When attackers breach a database and steal hashed passwords, they don’t need to guess passwords in real-time. Instead, they use rainbow…
The Salt That Breaks the Rainbow — Defeating Password Cracking with One Simple Ingredient
When attackers breach a database and steal hashed passwords, they don’t need to guess passwords in real-time. Instead, they use rainbow tables — precomputed lookup tables that map plaintext passwords to their cryptographic hashes. This trades storage space for processing speed, allowing attackers to crack passwords almost instantly, offline, without triggering any account lockouts.

But there’s a simple, elegant defense: salting. In this post, I’ll break down exactly how rainbow table attacks work and why adding a random string to each password before hashing makes those precomputed tables completely useless.
Part 1: How a Rainbow Table Attack Works
Let’s visualize the attack flow step by step.
┌────────────────────────────────────────────────────────────────────────┐
│ RAINBOW TABLE ATTACK FLOW │
└────────────────────────────────────────────────────────────────────────┘
BEFORE THE ATTACK (Precomputation Phase)
┌─────────────────────────────────────────────────────────────────┐
│ Attacker computes millions of plaintext → hash pairs offline │
│ │
│ "password" ──MD5──► 5f4dcc3b5aa765d61d8327deb882cf99 │
│ "123456" ──MD5──► e10adc3949ba59abbe56e057f20f883e │
│ "letmein" ──MD5──► 0d107d09f5bbe40cade3de5c71e9e9b7 │
│ ... (billions more) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ RAINBOW TABLE STORED │
│ ┌──────────────────────────┬────────────────────────────────┐ │
│ │ Plaintext │ Hash (MD5) │ │
│ ├──────────────────────────┼────────────────────────────────┤ │
│ │ "password" │ 5f4dcc3b5aa765d61d8327deb8.. │ │
│ │ "123456" │ e10adc3949ba59abbe56e057f2.. │ │
│ │ "letmein" │ 0d107d09f5bbe40cade3de5c71.. │ │
│ │ ... │ ... │ │
│ └──────────────────────────┴────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
DURING THE ATTACK (Database Breach)
┌─────────────────────────────────────────────────────────────────┐
│ Attacker steals hashed password database: │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ user@email.com │ 5f4dcc3b5aa765d61d8327deb882cf99 │ │
│ │ admin@site.com │ 0d107d09f5bbe40cade3de5c71e9e9b7 │ │
│ │ bob@company.com │ e10adc3949ba59abbe56e057f20f883e │ │
│ └────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Attacker cross-references stolen hashes against Rainbow Table │
│ │
│ Hash: 5f4dcc3b5aa765d61d8327deb882cf99 ───► "password" ✅ │
│ Hash: 0d107d09f5bbe40cade3de5c71e9e9b7 ───► "letmein" ✅ │
│ Hash: e10adc3949ba59abbe56e057f20f883e ───► "123456" ✅ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 🔓 ATTACKER NOW HAS PLAINTEXT PASSWORDS! 🔓 │
│ │
│ user@email.com → "password" (Compromised) │
│ admin@site.com → "letmein" (Compromised) │
│ bob@company.com → "123456" (Compromised) │
└─────────────────────────────────────────────────────────────────┘
Why This Works
The attack succeeds because the same password always produces the same hash. When “password” is hashed with MD5, it always generates 5f4dcc3b5aa765d61d8327deb882cf99. If multiple users choose "password", their hashes will be identical — and one match in the rainbow table cracks all of them.
Part 2: How Salting Defeats Rainbow Tables
Now let’s see how salting changes everything.
┌────────────────────────────────────────────────────────────────────────┐
│ UNSALTED vs SALTED PASSWORD STORAGE │
└────────────────────────────────────────────────────────────────────────┘
UNSALTED (Vulnerable) SALTED (Secure)
───────────────────── ────────────────
Password: "password" Password: "password"
┌────────────────────┐ ┌──────────────────────────┐
│ MD5("password") │ │ Random Salt (per user) │
│ = 5f4dcc3b5aa.. │ │ = "xY9#mQ2$" │
└────────────────────┘ └──────────────────────────┘
│ │
▼ ▼
┌────────────────────┐ ┌──────────────────────────┐
│ Same hash for │ │ MD5("password" + "xY9..")│
│ EVERY user using │ │ = "a7f3k9n4m2b6x8..." │
│ "password" │ └──────────────────────────┘
└────────────────────┘ │
▼
❌ ONE match in rainbow table ✅ UNIQUE hash PER user
cracks ALL users with same password (rainbow table useless)
The Key Difference
- Without salt: Same password → Same hash → One rainbow table entry cracks every user
- With salt: Same password + different salts → Different hashes → No precomputed table can predict these
Part 3: Same Password, Different Outcomes
Let’s illustrate with three users who all choose the same password.
┌───────────────────────────────────────────────────────────────────────────┐
│ "password123" → DIFFERENT OUTCOMES WITH/WITHOUT SALTING │
└───────────────────────────────────────────────────────────────────────────┘
USER 1 USER 2 USER 3
─────── ─────── ───────
Password: "pass123" Password: "pass123" Password: "pass123"
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ No Salt │ │ Salt A │ │ Salt B │
│ │ │ "9s#2" │ │ "mX@7" │
└───────────┘ └───────────┘ └───────────┘
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ 5f4dcc3b │ │ 8k2m9n4x │ │ 3pLq7v1y │
│ 5aa765d6 │ │ 7b3s6t2p │ │ 5w9R4f8Z │
│ 1d8327de │ │ 1h9q8r0d │ │ 2c6X0a4N │
│ b882cf99 │ │ 5m7v3g1w │ │ 7k1W3d9Q │
└───────────┘ └───────────┘ └───────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ ❌ Rainbow Table │ │ ✅ Rainbow Table │ │ ✅ Rainbow Table │
│ MATCHES! │ │ NO MATCH! │ │ NO MATCH! │
│ "password" found │ │ Hash is unique │ │ Hash is unique │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
What Just Happened?
- User 1 (unsalted):
MD5("pass123")=5f4dcc3b...→ Found in rainbow table → Cracked instantly - User 2 (salted):
MD5("pass123" + "9s#2")=8k2m9n4x...→ Not in any rainbow table → Safe - User 3 (salted):
MD5("pass123" + "mX@7")=3pLq7v1y...→ Not in any rainbow table → Safe
Part 4: The Math Behind the Protection
┌───────────────────────────────────────────────────────────────────────────┐
│ WHY RAINBOW TABLES BECOME IMPOSSIBLE │
└───────────────────────────────────────────────────────────────────────────┘
WITHOUT SALT (1 user)
┌─────────────────────────────────────────────────────────────────────┐
│ Precompute: 1 billion passwords × 1 hash = 1 billion hashes │
│ Storage: ~100 GB (MD5) │
│ Attack: Compare stolen hash → match found quickly │
└─────────────────────────────────────────────────────────────────────┘
WITH SALT (1,000,000 users)
┌─────────────────────────────────────────────────────────────────────┐
│ Precompute: 1 billion passwords × 1,000,000 salts = 1,000,000 │
│ billion hashes (1 quadrillion) │
│ │
│ Storage: ~100,000 TB (impossible!) │
│ │
│ Attack: Attacker would need to generate hashes for EACH user's │
│ salt individually in real time → brute force becomes │
│ infeasible (takes thousands of years) │
└─────────────────────────────────────────────────────────────────────┘
Why Salting Is Mathematically Devastating for Attackers
Without salt, an attacker needs:
- 1 hash per password → Precompute once, crack millions
With salt, an attacker needs:
- N hashes per password (where N = number of users) → Precompute for EVERY user individually
For a database with 1 million users, the attack becomes 1 million times harder. And since salts are random (often 16+ bytes), the permutations are astronomical.
Part 5: Defense Strategy Hierarchy
┌──────────────────────────────────────────────────────────────────────────┐
│ DEFENSE STRATEGY HIERARCHY │
└──────────────────────────────────────────────────────────────────────────┘
1st LAYER: SALING (Mandatory)
┌─────────────────────────────────────────────────────────────────────┐
│ ✅ Use a UNIQUE random salt per user (at least 16 bytes) │
│ ✅ Store salt alongside hash in the database │
│ ✅ Never reuse salts across users │
└─────────────────────────────────────────────────────────────────────┘
│
▼
2nd LAYER: MODERN HASHING ALGORITHMS
┌─────────────────────────────────────────────────────────────────────┐
│ ✅ bcrypt (built-in salt + adaptive work factor) │
│ ✅ scrypt (memory-hard + CPU intensive) │
│ ✅ Argon2 (winner of Password Hashing Competition) │
│ ✅ PBKDF2 (NIST recommended, like Rfc2898DeriveBytes.Pbkdf2) │
│ ❌ MD5, SHA-1, SHA-256 (without salt/iterations) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
3rd LAYER: ADDITIONAL BEST PRACTICES
┌─────────────────────────────────────────────────────────────────────┐
│ ✅ Enforce strong password policies (length + complexity) │
│ ✅ Use Multi-Factor Authentication (MFA) │
│ ✅ Implement rate limiting and account lockouts │
│ ✅ Regularly audit and rotate security parameters │
└─────────────────────────────────────────────────────────────────────┘
Implementation Example: PBKDF2 in C
Here’s how to implement salting correctly in .NET using Rfc2898DeriveBytes.Pbkdf2:
using System.Security.Cryptography;
using System.Text;
public class PasswordHasher
{
private const int ITERATIONS = 210_000; // OWASP 2024 recommendation
private const int HASH_SIZE = 32; // 256 bits for AES-256
private const int SALT_SIZE = 16; // 128 bits
public static string HashPassword(string password)
{
// 1. Generate a unique random salt for this user
byte[] salt = RandomNumberGenerator.GetBytes(SALT_SIZE);
// 2. Derive the hash using PBKDF2
byte[] hash = Rfc2898DeriveBytes.Pbkdf2(
password,
salt,
ITERATIONS,
HashAlgorithmName.SHA256,
HASH_SIZE
);
// 3. Store salt + hash together (common format: salt:hash)
return $"{Convert.ToBase64String(salt)}:{Convert.ToBase64String(hash)}";
}
public static bool VerifyPassword(string password, string stored)
{
var parts = stored.Split(':');
byte[] salt = Convert.FromBase64String(parts[0]);
byte[] storedHash = Convert.FromBase64String(parts[1]);
byte[] testHash = Rfc2898DeriveBytes.Pbkdf2(
password,
salt,
ITERATIONS,
HashAlgorithmName.SHA256,
HASH_SIZE
);
// 4. Constant-time comparison prevents timing attacks
return CryptographicOperations.FixedTimeEquals(testHash, storedHash);
}
}
Summary Table

Key Takeaway
┌─────────────────────────────────────────────────────────────┐
│ RAINBOW TABLE: "One hash to rule them all" │
│ SALTING: "One hash per user, per password" │
│ │
│ 💡 Salting transforms a single target into millions of │
│ unique, uncrackable targets, making precomputation │
│ economically impossible. │
└─────────────────────────────────────────────────────────────┘
Final Thoughts
Rainbow table attacks are a threat from a bygone era. The defenses are well-understood and easy to implement:
- Always use a unique salt per user — This alone defeats rainbow tables
- Use modern algorithms — bcrypt, scrypt, Argon2, or PBKDF2
- Use high iteration counts — Make brute force computationally expensive
- Store salt and hash together — It’s safe and convenient
Never store passwords in plain text. Never use unsalted MD5 or SHA-1 for passwords. The technology is freely available and battle-tested.
Have questions about implementing password hashing in your application? Drop a comment below!
메타데이터
- post_id
- 2ee60127a2f5
- slug
- the-salt-that-breaks-the-rainbow-defeating-password-cracking-with-one-simple-ingredient-2ee60127a2f5
- url
- https://medium.com/@rakibul.h.rabbi/the-salt-that-breaks-the-rainbow-defeating-password-cracking-with-one-simple-ingredient-2ee60127a2f5
- canonical_url
- https://medium.com/@rakibul.h.rabbi/the-salt-that-breaks-the-rainbow-defeating-password-cracking-with-one-simple-ingredient-2ee60127a2f5
- author_url
- https://medium.com/@rakibul.h.rabbi
- status
- ok
- fetched_at
- 2026-07-21 21:51:53