Verwendung eines Einweg-Hash mit vorhersagbarem Salt
Beschreibung
Die Verwendung eines Einweg-Hash mit vorhersagbarem Salt ist eine kryptografische Schwachstelle, bei der Software einen Einweg-Kryptografie-Hash für Daten verwendet, die irreversibel bleiben sollten (wie Passwörter), aber einen vorhersagbaren Salt während des Hashing-Prozesses implementiert. Ein Salt soll ein zufälliger Wert sein, der für jede Hash-Operation eindeutig ist und Vorberechnungsangriffe verhindert. Wenn der Salt vorhersagbar ist - wie die Verwendung des Benutzernamens, eines konstanten Werts oder eines von benutzersteuerbare Daten abgeleiteten Werts - können Angreifer Hashes für häufige Passwörter kombiniert mit bekannten Salts vorberechnen, was den Zweck des Saltings zunichtemacht.
Risiko
Vorhersagbare Salts schwächen die Passwortsicherheit erheblich. Angreifer können gezielte Rainbow Tables mit bekannten oder vorhersagbaren Salt-Werten erstellen. Wenn der Salt für alle Benutzer konstant ist, kompromittiert ein einziger Rainbow-Table-Angriff alle Passwörter. Wenn der Salt der Benutzername ist, können Angreifer Tabellen für häufige Benutzernamen vorberechnen. Selbst mit benutzerspezifischen Salts können Angreifer, wenn das Salt-Muster vorhersagbar ist (wie sequenzielle Nummern), gezielte Tabellen berechnen. Während Salts generische Rainbow-Table-Angriffe verhindern, bleiben vorhersagbare Salts anfällig für gezielte Wörterbuchangriffe, besonders mit moderner GPU-beschleunigter Hash-Crackning. Die Schwäche wird verstärkt, wenn sie mit schnellen Hash-Funktionen wie MD5 oder SHA-1 kombiniert wird.
Lösung
Verwenden Sie kryptografisch sichere Zufallszahlengeneratoren, um eindeutige Salts für jedes Passwort zu erstellen. Speichern Sie den Salt zusammen mit dem Hash. Verwenden Sie adaptive Passwort-Hashing-Funktionen wie bcrypt, scrypt, Argon2 oder PBKDF2, die Salting einbauen und rechenintensiv gestaltet sind. Diese Funktionen machen sowohl Rainbow-Table-Angriffe als auch Brute-Force-Angriffe unpraktisch. Konfigurieren Sie den Arbeitsfaktor hoch genug, um Angriffe kostspielig zu machen, während er für legitime Authentifizierung noch akzeptabel ist. Erhöhen Sie regelmäßig den Arbeitsfaktor mit steigender Rechenleistung.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Zugriffskontrolle | Bereich: Zugriffskontrolle Schutzmechanismus umgehen - Vorhersagbare Salts ermöglichen Vorberechnungsangriffe, die Passwörter wiederherstellen können. |
| Vertraulichkeit | Bereich: Vertraulichkeit Anwendungsdaten lesen - Wiederhergestellte Passwörter können Zugang zu Benutzerkonten und sensiblen Daten ermöglichen. |
Beispielcode + Lösungscode
Anfälliger Code
# Anfällig: Verwendung des Benutzernamens als Salt
import hashlib
def vulnerable_hash_password(username, password):
# Anfällig: Benutzername ist vorhersagbar und Angreifern bekannt
salt = username
# Angreifer kann Hashes für häufige Benutzernamen vorberechnen
hash_input = salt + password
password_hash = hashlib.sha256(hash_input.encode()).hexdigest()
return password_hash
# Anfällig: Verwendung eines konstanten Salts
CONSTANT_SALT = "MySuperSecretSalt123" # Gleich für alle Benutzer!
def vulnerable_constant_salt(password):
# Anfällig: Eine Rainbow Table greift ALLE Passwörter an
hash_input = CONSTANT_SALT + password
return hashlib.sha256(hash_input.encode()).hexdigest()
# Anfällig: Verwendung eines vorhersagbar abgeleiteten Salts
def vulnerable_derived_salt(user_id, password):
# Anfällig: Salt von sequenzieller Benutzer-ID abgeleitet
salt = f"salt_{user_id}" # user_id ist 1, 2, 3...
# Angreifer können alle möglichen Salts aufzählen
return hashlib.sha256((salt + password).encode()).hexdigest()
<?php
// Anfällig: Fest kodierter Salt (CVE-2008-4905-Muster)
define('PASSWORD_SALT', 'blog_secret_2008');
function vulnerable_hash($password) {
// Anfällig: Gleicher Salt für alle Passwörter
return md5(PASSWORD_SALT . $password);
}
// Anfällig: Verwendung der E-Mail-Domain als Salt
function vulnerable_email_salt($email, $password) {
// Anfällig: Domain ist vorhersagbar
$domain = explode('@', $email)[1]; // z.B. "gmail.com"
// Angreifer kann Tabellen für häufige Domains erstellen
return hash('sha256', $domain . $password);
}
// Anfällig: Sequenzieller Salt (CVE-2002-1657-Muster)
function vulnerable_sequential_salt($user_id, $password) {
// Anfällig: Vorhersagbare Sequenz
$salt = $user_id; // 1, 2, 3, 4...
return hash('sha256', $salt . $password);
}
?>
// Anfällig: Java mit vorhersagbarem Salt
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;
public class VulnerableHashing {
// Anfällig: Statischer konstanter Salt
private static final String SALT = "StaticSalt2024";
public String vulnerableHash(String password) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
// Anfällig: Gleicher Salt für alle
String salted = SALT + password;
byte[] hash = md.digest(salted.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash);
} catch (Exception e) {
return null;
}
}
// Anfällig: Salt basierend auf Benutzerattribut
public String vulnerableUserSalt(User user, String password) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
// Anfällig: Benutzername ist vorhersagbar
String salted = user.getUsername() + password;
byte[] hash = md.digest(salted.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash);
} catch (Exception e) {
return null;
}
}
}
// Anfällig: C mit konstantem Salt (CVE-2001-0967-Muster)
#include <string.h>
#include <openssl/sha.h>
#define CONSTANT_SALT "ServerSalt"
void vulnerable_hash(const char* password, unsigned char* output) {
char buffer[256];
// Anfällig: Konstanter Salt
snprintf(buffer, sizeof(buffer), "%s%s", CONSTANT_SALT, password);
SHA256((unsigned char*)buffer, strlen(buffer), output);
}
// Anfällig: Salt von Zeitstempel mit niedriger Auflösung
void vulnerable_time_salt(const char* password, unsigned char* output) {
char buffer[256];
time_t now = time(NULL);
// Anfällig: Nur ~31M mögliche Werte pro Jahr
// Angreifer kann Zeitstempel um Kontoerstellung herum aufzählen
snprintf(buffer, sizeof(buffer), "%ld%s", now, password);
SHA256((unsigned char*)buffer, strlen(buffer), output);
}
Behobener Code
# Behoben: Ordentliches Passwort-Hashing mit zufälligem Salt verwenden
import bcrypt
import argon2
import secrets
# Behoben: Verwendung von bcrypt (empfohlen)
def secure_hash_bcrypt(password):
# bcrypt generiert zufälligen Salt intern
# Arbeitsfaktor einstellbar für Sicherheit/Leistung-Balance
salt = bcrypt.gensalt(rounds=12)
password_hash = bcrypt.hashpw(password.encode(), salt)
return password_hash
def verify_bcrypt(password, stored_hash):
return bcrypt.checkpw(password.encode(), stored_hash)
# Behoben: Verwendung von Argon2 (aktuelle Empfehlung)
def secure_hash_argon2(password):
ph = argon2.PasswordHasher(
time_cost=3, # Anzahl der Iterationen
memory_cost=65536, # 64MB Speicherverbrauch
parallelism=4 # Parallele Threads
)
return ph.hash(password)
def verify_argon2(password, stored_hash):
ph = argon2.PasswordHasher()
try:
return ph.verify(stored_hash, password)
except argon2.exceptions.VerifyMismatchError:
return False
# Behoben: Manueller Ansatz mit kryptografisch zufälligem Salt
import hashlib
def secure_hash_manual(password):
# Behoben: Kryptografisch zufälligen Salt generieren
salt = secrets.token_bytes(32) # 256 Bits Zufälligkeit
# Langsame Hash-Funktion verwenden
# Hinweis: Besser ist immer noch bcrypt/argon2
hash_value = hashlib.pbkdf2_hmac(
'sha256',
password.encode(),
salt,
iterations=100000 # Hohe Iterationszahl
)
# Salt + Hash zurückgeben (Salt für Verifizierung benötigt)
return salt + hash_value
<?php
// Behoben: PHP Passwort-Hashing (PHP 5.5+)
function secure_hash($password) {
// Behoben: password_hash generiert zufälligen Salt automatisch
// Verwendet standardmäßig bcrypt (PASSWORD_DEFAULT)
return password_hash($password, PASSWORD_DEFAULT, ['cost' => 12]);
}
function secure_verify($password, $stored_hash) {
return password_verify($password, $stored_hash);
}
// Behoben: Verwendung von Argon2 (PHP 7.2+)
function secure_hash_argon2($password) {
return password_hash($password, PASSWORD_ARGON2ID, [
'memory_cost' => 65536,
'time_cost' => 4,
'threads' => 3
]);
}
// Behoben: Manueller Ansatz mit ordentlichem Salt
function secure_manual_hash($password) {
// Behoben: Kryptografisch zufälliger Salt
$salt = random_bytes(32);
// PBKDF2 mit hohen Iterationen verwenden
$hash = hash_pbkdf2('sha256', $password, $salt, 100000, 32, true);
// Kodierten Salt und Hash zurückgeben
return base64_encode($salt) . '$' . base64_encode($hash);
}
?>
// Behoben: Java mit ordentlichem Passwort-Hashing
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.SecureRandom;
public class SecureHashing {
// Behoben: Verwendung von BCrypt
private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
public String secureBcryptHash(String password) {
// BCrypt generiert zufälligen Salt intern
return encoder.encode(password);
}
public boolean secureBcryptVerify(String password, String hash) {
return encoder.matches(password, hash);
}
// Behoben: Verwendung von PBKDF2 mit zufälligem Salt
public String securePbkdf2Hash(String password) throws Exception {
// Behoben: Zufälligen Salt generieren
SecureRandom random = new SecureRandom();
byte[] salt = new byte[32];
random.nextBytes(salt);
// Hohe Iterationszahl
int iterations = 100000;
PBEKeySpec spec = new PBEKeySpec(
password.toCharArray(), salt, iterations, 256);
SecretKeyFactory factory =
SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] hash = factory.generateSecret(spec).getEncoded();
// Iterationen:Salt:Hash zurückgeben
return iterations + ":" + bytesToHex(salt) + ":" + bytesToHex(hash);
}
}
// Behoben: C mit ordentlicher Salt-Generierung
#include <string.h>
#include <openssl/rand.h>
#include <openssl/evp.h>
#define SALT_LENGTH 32
#define HASH_LENGTH 32
#define ITERATIONS 100000
int secure_hash(const char* password,
unsigned char* salt_out,
unsigned char* hash_out) {
// Behoben: Kryptografisch zufälligen Salt generieren
if (RAND_bytes(salt_out, SALT_LENGTH) != 1) {
return -1; // RNG-Fehler
}
// PBKDF2 mit hohen Iterationen verwenden
if (PKCS5_PBKDF2_HMAC(password, strlen(password),
salt_out, SALT_LENGTH,
ITERATIONS,
EVP_sha256(),
HASH_LENGTH, hash_out) != 1) {
return -1;
}
return 0;
}
int secure_verify(const char* password,
const unsigned char* stored_salt,
const unsigned char* stored_hash) {
unsigned char computed_hash[HASH_LENGTH];
// Hash mit gespeichertem Salt neu berechnen
if (PKCS5_PBKDF2_HMAC(password, strlen(password),
stored_salt, SALT_LENGTH,
ITERATIONS,
EVP_sha256(),
HASH_LENGTH, computed_hash) != 1) {
return 0;
}
// Konstantzeit-Vergleich
return CRYPTO_memcmp(computed_hash, stored_hash, HASH_LENGTH) == 0;
}
CVE-Beispiele
- CVE-2008-4905: Blogging-Software verwendete fest kodierten Salt für Passwort-Hashing.
- CVE-2002-1657: Datenbankserver verwendete Benutzernamen als Salt, was gezielte Brute-Force-Angriffe ermöglichte.
- CVE-2001-0967: Server verwendete konstanten Salt für alle Passwörter.
- CVE-2005-0408: Vorhersagbare MD5-Hashes mit konstanten Werten plus Benutzernamen ermöglichten Authentifizierungsumgehung.
Referenzen
- MITRE Corporation. "CWE-760: Use of a One-Way Hash with a Predictable Salt." https://cwe.mitre.org/data/definitions/760.html
- OWASP. "Password Storage Cheat Sheet."
- NIST SP 800-132. "Recommendation for Password-Based Key Derivation."