Use of Weak Hash
Description
Use of Weak Hash is a vulnerability that occurs when a product uses a hashing algorithm that does not meet security requirements, allowing attackers to determine original input values, find hash collisions, or perform birthday attacks more efficiently than brute force methods would allow. A cryptographic hash function must be computationally infeasible to invert (one-way), deterministically produce the same output for identical inputs, and resist preimage, second preimage, and birthday attacks. Weak hash functions including MD5, SHA-1, and unsalted SHA-2 variants fail to meet these requirements for security-critical applications, particularly password storage where rainbow table attacks are highly effective.
Risk
Weak hash functions expose applications to multiple attack vectors. Rainbow tables containing precomputed hashes for common passwords can instantly reverse MD5 or SHA-1 password hashes without salting. Collision attacks on MD5 and SHA-1 enable certificate forgery, allowing attackers to create malicious certificates with the same hash as legitimate ones. Fast hash functions like MD5, SHA-1, and even SHA-256 enable rapid brute force attacks using GPUs or specialized hardware. For password storage, modern GPUs can compute billions of MD5 hashes per second. The risk extends beyond password cracking: integrity verification using weak hashes can be bypassed by crafting colliding inputs, digital signatures become forgeable, and content-addressed storage systems become vulnerable to manipulation. These attacks are not theoretical - they have been demonstrated practically and exploited in the wild.
Solution
Use cryptographic hash functions appropriate for the specific use case. For password storage, use adaptive hash functions with configurable work factors: bcrypt, scrypt, or Argon2id. These functions incorporate salts automatically and allow tuning computational cost to remain secure as hardware improves. For general integrity verification and digital signatures, use SHA-256 or SHA-3. Never use MD5, SHA-1, or unsalted hashes for security purposes. When upgrading from weak hashes, implement gradual migration that re-hashes passwords on successful login. For HMAC construction, even MD5 and SHA-1 are acceptable due to HMAC's security properties, but SHA-256 is preferred. Configure appropriate work factors considering both security and denial-of-service risks from expensive hash operations.
Common Consequences
| Impact | Details |
|---|---|
| Access Control | Scope: Access Control Attackers can bypass authentication by recovering passwords from weak hashes using rainbow tables, brute force attacks, or precomputed dictionaries. |
| Integrity | Scope: Integrity Collision attacks on weak hashes enable attackers to forge data with matching hashes, bypassing integrity verification and potentially allowing code or certificate substitution. |
| Non-Repudiation | Scope: Non-Repudiation Digital signatures using weak hashes can be forged through collision attacks, undermining the ability to prove authorship or detect tampering. |
Example Code
Vulnerable Code (Python/Java)
The following examples demonstrate use of weak hash functions:
# Vulnerable: Using weak hash functions
import hashlib
# Vulnerable: MD5 for password storage
def vulnerable_store_password(password):
# Vulnerable: MD5 is cryptographically broken
# Rainbow tables can crack these instantly
return hashlib.md5(password.encode()).hexdigest()
def vulnerable_verify_password(password, stored_hash):
return hashlib.md5(password.encode()).hexdigest() == stored_hash
# Vulnerable: SHA-1 for integrity
def vulnerable_compute_checksum(data):
# Vulnerable: SHA-1 has practical collision attacks
return hashlib.sha1(data).hexdigest()
# Vulnerable: Unsalted SHA-256 for passwords
def vulnerable_sha256_password(password):
# Vulnerable: Without salt, identical passwords produce identical hashes
# Rainbow tables exist for SHA-256 passwords too
return hashlib.sha256(password.encode()).hexdigest()
# Vulnerable: CRC32 for integrity (not cryptographic at all)
import zlib
def vulnerable_crc_checksum(data):
# Vulnerable: CRC32 is NOT a cryptographic hash
# Trivial to find collisions
return hex(zlib.crc32(data) & 0xffffffff)
# Vulnerable: Simple salted hash with fast algorithm
def vulnerable_salted_md5(password, salt):
# Vulnerable: MD5 is too fast, even with salt
# GPUs can try billions per second
return hashlib.md5((salt + password).encode()).hexdigest()
# Vulnerable: Single iteration of any hash
def vulnerable_single_hash(password):
# Vulnerable: Single iteration is too fast
return hashlib.sha512(password.encode()).hexdigest()
// Vulnerable: Using weak hash functions in Java
import java.security.*;
import java.util.*;
public class VulnerableHashing {
// Vulnerable: MD5 for password storage
public String vulnerableMd5Password(String password) throws Exception {
// Vulnerable: MD5 is broken
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] hash = md.digest(password.getBytes("UTF-8"));
return bytesToHex(hash);
}
// Vulnerable: SHA-1 for integrity
public String vulnerableSha1Checksum(byte[] data) throws Exception {
// Vulnerable: SHA-1 has collision attacks
MessageDigest md = MessageDigest.getInstance("SHA-1");
return bytesToHex(md.digest(data));
}
// Vulnerable: Unsalted SHA-256
public String vulnerableSha256(String password) throws Exception {
// Vulnerable: No salt means rainbow tables work
MessageDigest md = MessageDigest.getInstance("SHA-256");
return bytesToHex(md.digest(password.getBytes()));
}
// Vulnerable: Custom hash combination
public String vulnerableCustomHash(String password) throws Exception {
// Vulnerable: Combining weak hashes doesn't make them stronger
MessageDigest md5 = MessageDigest.getInstance("MD5");
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
byte[] md5Hash = md5.digest(password.getBytes());
byte[] sha1Hash = sha1.digest(password.getBytes());
// XORing weak hashes together
byte[] combined = new byte[16];
for (int i = 0; i < 16; i++) {
combined[i] = (byte)(md5Hash[i] ^ sha1Hash[i]);
}
return bytesToHex(combined);
}
// Vulnerable: Using hash for encryption
public byte[] vulnerableHashEncrypt(byte[] data, String key) throws Exception {
// Vulnerable: Hash functions are not encryption!
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] keyHash = md.digest(key.getBytes());
// XOR with hash - this is NOT secure encryption
byte[] result = new byte[data.length];
for (int i = 0; i < data.length; i++) {
result[i] = (byte)(data[i] ^ keyHash[i % keyHash.length]);
}
return result;
}
private String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
// Vulnerable: Using weak hash functions in C
#include <openssl/md5.h>
#include <openssl/sha.h>
#include <string.h>
#include <stdio.h>
// Vulnerable: MD5 password hash
void vulnerable_md5_password(const char *password, char *output) {
unsigned char hash[MD5_DIGEST_LENGTH];
// Vulnerable: MD5 is broken
MD5((unsigned char*)password, strlen(password), hash);
for (int i = 0; i < MD5_DIGEST_LENGTH; i++) {
sprintf(output + (i * 2), "%02x", hash[i]);
}
}
// Vulnerable: SHA-1 integrity check
void vulnerable_sha1_file(const char *filename, unsigned char *output) {
// Vulnerable: SHA-1 collision attacks are practical
FILE *f = fopen(filename, "rb");
SHA_CTX ctx;
SHA1_Init(&ctx);
unsigned char buffer[4096];
size_t bytes;
while ((bytes = fread(buffer, 1, sizeof(buffer), f)) > 0) {
SHA1_Update(&ctx, buffer, bytes);
}
SHA1_Final(output, &ctx);
fclose(f);
}
// Vulnerable: Simple salted hash
void vulnerable_salted_hash(const char *password, const char *salt,
char *output) {
char combined[256];
snprintf(combined, sizeof(combined), "%s%s", salt, password);
// Vulnerable: Single SHA-256 is too fast
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256((unsigned char*)combined, strlen(combined), hash);
for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
sprintf(output + (i * 2), "%02x", hash[i]);
}
}
Fixed Code (Python/Java)
# Fixed: Using secure hash functions
import bcrypt
import hashlib
import hmac
import secrets
# Fixed: bcrypt for password storage
def secure_store_password(password):
# Fixed: bcrypt with automatic salt and work factor
return bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
def secure_verify_password(password, stored_hash):
# Fixed: Constant-time comparison built into bcrypt
return bcrypt.checkpw(password.encode(), stored_hash)
# Fixed: Argon2 for password storage (preferred)
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
def secure_argon2_password(password):
# Fixed: Argon2id with memory-hard properties
ph = PasswordHasher(
time_cost=3,
memory_cost=65536, # 64 MB
parallelism=4
)
return ph.hash(password)
def secure_verify_argon2(password, hash):
ph = PasswordHasher()
try:
ph.verify(hash, password)
return True
except VerifyMismatchError:
return False
# Fixed: SHA-256 for integrity (where collision resistance matters less)
def secure_compute_checksum(data):
# Fixed: SHA-256 for general integrity
return hashlib.sha256(data).hexdigest()
# Fixed: SHA-3 for highest security
def secure_sha3_hash(data):
# Fixed: SHA-3 has different construction than SHA-2
return hashlib.sha3_256(data).hexdigest()
# Fixed: HMAC for keyed hashing
def secure_hmac(key, data):
# Fixed: HMAC provides authentication
return hmac.new(key, data, hashlib.sha256).hexdigest()
# Fixed: Constant-time comparison
def secure_compare_hashes(hash1, hash2):
# Fixed: Prevent timing attacks
return hmac.compare_digest(hash1, hash2)
# Fixed: PBKDF2 as alternative to bcrypt
def secure_pbkdf2_password(password, salt=None):
if salt is None:
salt = secrets.token_bytes(16)
# Fixed: High iteration count
dk = hashlib.pbkdf2_hmac(
'sha256',
password.encode(),
salt,
600000 # OWASP recommended minimum
)
return salt + dk
// Fixed: Using secure hash functions in Java
import org.mindrot.jbcrypt.BCrypt;
import javax.crypto.*;
import javax.crypto.spec.*;
import java.security.*;
import java.util.*;
public class SecureHashing {
// Fixed: bcrypt for password storage
public String secureHashPassword(String password) {
// Fixed: bcrypt with automatic salt and work factor
return BCrypt.hashpw(password, BCrypt.gensalt(12));
}
public boolean secureVerifyPassword(String password, String hash) {
// Fixed: Constant-time comparison
return BCrypt.checkpw(password, hash);
}
// Fixed: PBKDF2 for password-based key derivation
public byte[] securePbkdf2(String password, byte[] salt) throws Exception {
// Fixed: High iteration count
SecretKeyFactory factory = SecretKeyFactory.getInstance(
"PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(
password.toCharArray(),
salt,
600000, // High iterations
256
);
return factory.generateSecret(spec).getEncoded();
}
// Fixed: SHA-256 for integrity
public byte[] secureChecksum(byte[] data) throws Exception {
// Fixed: SHA-256 for general integrity
MessageDigest md = MessageDigest.getInstance("SHA-256");
return md.digest(data);
}
// Fixed: SHA-3 for highest security
public byte[] secureSha3(byte[] data) throws Exception {
// Fixed: SHA3-256
MessageDigest md = MessageDigest.getInstance("SHA3-256");
return md.digest(data);
}
// Fixed: HMAC for keyed hashing
public byte[] secureHmac(byte[] key, byte[] data) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA256");
mac.init(keySpec);
return mac.doFinal(data);
}
// Fixed: Constant-time hash comparison
public boolean secureCompare(byte[] hash1, byte[] hash2) {
// Fixed: MessageDigest.isEqual is constant-time
return MessageDigest.isEqual(hash1, hash2);
}
// Fixed: Generate secure salt
public byte[] generateSalt() {
byte[] salt = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(salt);
return salt;
}
}
// Fixed: Using secure hash functions in C
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <argon2.h>
#include <string.h>
// Fixed: Argon2id for password hashing
int secure_hash_password(const char *password, char *output, size_t output_len) {
uint8_t salt[16];
// Generate random salt
if (RAND_bytes(salt, sizeof(salt)) != 1) {
return -1;
}
// Fixed: Argon2id with secure parameters
int result = argon2id_hash_encoded(
3, // time_cost
65536, // memory_cost (64 MB)
4, // parallelism
password, strlen(password),
salt, sizeof(salt),
32, // hash length
output, output_len
);
return result == ARGON2_OK ? 0 : -1;
}
int secure_verify_password(const char *password, const char *hash) {
// Fixed: Argon2 verification with constant-time comparison
return argon2id_verify(hash, password, strlen(password)) == ARGON2_OK;
}
// Fixed: SHA-256 for integrity
int secure_sha256(const unsigned char *data, size_t len,
unsigned char *output) {
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
// Fixed: SHA-256 using EVP interface
EVP_DigestInit_ex(ctx, EVP_sha256(), NULL);
EVP_DigestUpdate(ctx, data, len);
unsigned int hash_len;
EVP_DigestFinal_ex(ctx, output, &hash_len);
EVP_MD_CTX_free(ctx);
return hash_len;
}
// Fixed: SHA-3 for highest security
int secure_sha3_256(const unsigned char *data, size_t len,
unsigned char *output) {
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
// Fixed: SHA3-256
EVP_DigestInit_ex(ctx, EVP_sha3_256(), NULL);
EVP_DigestUpdate(ctx, data, len);
unsigned int hash_len;
EVP_DigestFinal_ex(ctx, output, &hash_len);
EVP_MD_CTX_free(ctx);
return hash_len;
}
// Fixed: Constant-time comparison
int secure_compare(const unsigned char *a, const unsigned char *b, size_t len) {
// Fixed: CRYPTO_memcmp is constant-time
return CRYPTO_memcmp(a, b, len) == 0;
}
// Fixed: PBKDF2 for key derivation
int secure_pbkdf2(const char *password, const unsigned char *salt,
size_t salt_len, unsigned char *key, size_t key_len) {
// Fixed: High iteration count
return PKCS5_PBKDF2_HMAC(
password, strlen(password),
salt, salt_len,
600000, // High iterations
EVP_sha256(),
key_len, key
);
}
The fix uses bcrypt, Argon2, or PBKDF2 for passwords, and SHA-256/SHA-3 for integrity verification.
Exploited in the Wild
LinkedIn Password Breach (2012)
6.5 million LinkedIn password hashes were leaked, stored as unsalted SHA-1. Most were cracked within days using rainbow tables and GPU-based attacks.
Flame Malware MD5 Collision (2012)
The Flame malware used an MD5 collision attack to forge Windows Update certificates, allowing malware distribution that appeared to come from Microsoft.
Tools to Test/Exploit
-
hashcat — GPU-accelerated password hash cracker supporting many weak algorithms.
-
John the Ripper — Classic password cracker with extensive format support.
-
Rainbow Crack — Rainbow table generation and lookup.
CVE Examples
-
CVE-2005-4900 — SHA-1 lacks collision resistance.
-
CVE-2012-6707 — Product uses MD5-based password algorithm.
-
CVE-2019-14855 — Certificate signature forgery via SHA-1 collisions.
References
-
MITRE Corporation. "CWE-328: Use of Weak Hash." Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/328.html
-
OWASP Foundation. "Password Storage Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
-
NIST. "Digital Identity Guidelines." SP 800-63B. https://pages.nist.gov/800-63-3/sp800-63b.html