What is Weak Password Hashing?
Weak password hashing occurs when an application stores user passwords using cryptographic hash functions that are too fast (MD5, SHA-1, SHA-256, SHA-512), stores them in plaintext, or uses hashing without a unique per-password salt. When an attacker obtains a database dump, they can crack weak hashes at billions of guesses per second using GPU-accelerated tools.
MD5 hashes can be cracked at ~200 billion hashes/sec on commodity hardware. An 8-character password database hashed with MD5 can be fully cracked in minutes. Password-specific algorithms (bcrypt, Argon2, PBKDF2) are intentionally slow and memory-hard, making large-scale cracking computationally infeasible.
How exploitation works
A database breach exposes a password table:
| username | password_hash |
|----------|----------------------------------|
| alice | 5f4dcc3b5aa765d61d8327deb882cf99 | ← MD5("password")
| bob | 202cb962ac59075b964b07152d234b70 | ← MD5("123")
An attacker runs these hashes against a precomputed rainbow table or dictionary attack tool. Common passwords like “password” and “123” are cracked instantly. Without salt, identical passwords produce identical hashes — cracking one hash cracks all accounts with the same password.
Vulnerable code examples
C# — MD5 password hash
// VULNERABLE: MD5 is a fast hash — trivially crackable
public string HashPassword(string password)
{
using var md5 = MD5.Create();
byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(password));
return Convert.ToHexString(hash);
}
Java — SHA-256 without salt
// VULNERABLE: Even SHA-256 without per-user salt is vulnerable to rainbow tables
public String hashPassword(String password) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
}
PHP — md5/sha1
// VULNERABLE: Never use md5() or sha1() for passwords
$hash = md5($password);
$hash = sha1($password);
Secure code examples
C# — ASP.NET Core Identity (BCrypt)
// SECURE: ASP.NET Core Identity uses PBKDF2 with 100,000 iterations by default
// Or use BCrypt.Net-Next directly:
using BCrypt.Net;
public string HashPassword(string password)
{
return BCrypt.HashPassword(password, workFactor: 12); // Cost factor 12 ≈ 250ms
}
public bool VerifyPassword(string password, string hash)
{
return BCrypt.Verify(password, hash);
}
Java — BCrypt with Spring Security
// SECURE: BCryptPasswordEncoder is the Spring Security standard
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // Cost factor 12
}
// Usage:
String hash = passwordEncoder.encode(rawPassword);
boolean valid = passwordEncoder.matches(rawPassword, storedHash);
Python — Argon2
# SECURE: Argon2 is the winner of the Password Hashing Competition
# pip install argon2-cffi
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)
def hash_password(password: str) -> str:
return ph.hash(password)
def verify_password(stored_hash: str, password: str) -> bool:
return ph.verify(stored_hash, password)
PHP — password_hash
// SECURE: PHP's built-in password_hash uses bcrypt by default
$hash = password_hash($password, PASSWORD_ARGON2ID, [
'memory_cost' => 65536,
'time_cost' => 4,
'threads' => 1,
]);
$valid = password_verify($password, $hash);
What Offensive360 detects
- MD5/SHA-1/SHA-2 for passwords — Direct use of fast hash functions on password values
- Missing salt — Hash operations on passwords without a per-password random salt
- Plaintext password storage — Passwords written to databases, logs, or files without hashing
- Hardcoded or static salts — Salt values that are constant across users, negating rainbow table protection
- Custom password hashing — Homegrown schemes that avoid established password hashing algorithms
Remediation guidance
-
Use a dedicated password hashing algorithm — Argon2id (recommended), bcrypt, or PBKDF2. Never use MD5, SHA-1, or SHA-256 for passwords.
-
Use your framework’s built-in password library — ASP.NET Core Identity, Spring Security’s
BCryptPasswordEncoder, Django’smake_password, and PHP’spassword_hash()are all correct by default. -
Set an appropriate work factor — bcrypt cost 12, Argon2id with 64MB memory and 3 iterations, or PBKDF2 with 600,000 iterations (NIST 2023 recommendation).
-
Migrate legacy hashes — On next login, detect old hash format, verify the plaintext against the old hash, then re-hash with the new algorithm.
-
Never log passwords — Ensure passwords are not written to logs, error messages, or debug output at any point in the authentication flow.