Verwendung eines Einweg-Hash ohne Salt
Beschreibung
Die Verwendung eines Einweg-Hash ohne Salt tritt auf, wenn Software einen Hash eines Passworts oder anderer sicherheitsrelevanter Daten mit einer kryptografischen Hash-Funktion generiert, aber keinen eindeutigen Salt-Wert einschließt. Ohne Salting erzeugen identische Passwörter identische Hashes, was Rainbow-Table-Angriffe ermöglicht, bei denen Angreifer vorberechnete Tabellen verwenden, um häufige Passwort-Hashes schnell umzukehren. Dies ermöglicht es Angreifern auch, Benutzer mit demselben Passwort durch Vergleich der Hash-Werte zu identifizieren.
Risiko
Nicht gesalzene Passwort-Hashes sind extrem anfällig für Rainbow-Table-Angriffe. Angreifer haben vorberechnete Tabellen für Milliarden häufiger Passwörter gegen Algorithmen wie MD5 und SHA-1. Ein einziger Datenbank-Breach legt alle Benutzer mit häufigen Passwörtern sofort offen. Ohne Salts können Angreifer Millionen von Passwörtern parallel knacken, indem sie sie gegen bekannte Hashes vergleichen. Der LinkedIn-Breach (2012) legte 6,5 Millionen nicht gesalzene SHA-1-Passwort-Hashes offen, die innerhalb von Tagen geknackt wurden.
Lösung
Verwenden Sie immer einen eindeutigen, zufällig generierten Salt für jeden Passwort-Hash. Verwenden Sie moderne Passwort-Hashing-Funktionen, die Salting automatisch handhaben (bcrypt, scrypt, Argon2). Der Salt sollte mindestens 16 Bytes kryptografisch zufälliger Daten sein. Speichern Sie den Salt zusammen mit dem Hash - Salts sind keine Geheimnisse. Verwenden Sie niemals einfache Hash-Funktionen wie MD5 oder SHA-1 für Passwörter. Verwenden Sie Key-Derivation-Funktionen mit konfigurierbaren Arbeitsfaktoren, um Brute-Force-Angriffe zu verlangsamen.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Vertraulichkeit | Bereich: Passwort-Offenlegung Rainbow-Table-Angriffe können nicht gesalzene Hashes für häufige Passwörter in Sekunden knacken. |
| Authentifizierung | Bereich: Kontoübernahme Geknackte Passwörter führen zu unbefugtem Kontozugriff. |
| Privatsphäre | Bereich: Passwortmuster-Erkennung Angreifer können Benutzer mit identischen Passwörtern über Systeme hinweg identifizieren. |
Beispielcode + Lösungscode
Anfälliger Code
# ANFÄLLIG: Passwort-Hashing ohne Salt
import hashlib
def hash_password_vulnerable(password):
# Einfacher Hash ohne Salt - anfällig für Rainbow Tables!
return hashlib.sha256(password.encode()).hexdigest()
def verify_password_vulnerable(password, stored_hash):
return hash_password_vulnerable(password) == stored_hash
# ANFÄLLIG: Verwendung von MD5 (schnell, kein Salt)
def hash_password_md5(password):
return hashlib.md5(password.encode()).hexdigest()
# ANFÄLLIG: Schwacher Versuch des Saltings - Salt wiederverwendet
GLOBAL_SALT = "myappsalt"
def hash_with_static_salt(password):
# Gleicher Salt für alle Benutzer - immer noch anfällig für Rainbow Tables!
return hashlib.sha256((GLOBAL_SALT + password).encode()).hexdigest()
// ANFÄLLIG: Java Passwort-Hashing ohne Salt
import java.security.MessageDigest;
import java.util.Base64;
public class VulnerablePasswordHash {
public String hashPassword(String password) throws Exception {
// Kein Salt - anfällig für Rainbow Tables!
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(password.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(hash);
}
// ANFÄLLIG: MD5 - schnell und ohne Salt
public String hashPasswordMD5(String password) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] hash = md.digest(password.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(hash);
}
// ANFÄLLIG: Statischer Salt
private static final String STATIC_SALT = "application_salt";
public String hashWithStaticSalt(String password) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(STATIC_SALT.getBytes("UTF-8"));
byte[] hash = md.digest(password.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(hash);
}
}
// ANFÄLLIG: Node.js Passwort-Hashing ohne Salt
const crypto = require('crypto');
function hashPasswordVulnerable(password) {
// Kein Salt!
return crypto.createHash('sha256').update(password).digest('hex');
}
// ANFÄLLIG: MD5 ohne Salt
function hashPasswordMD5(password) {
return crypto.createHash('md5').update(password).digest('hex');
}
// ANFÄLLIG: Gleicher Salt für alle
const GLOBAL_SALT = 'fixed_salt_for_app';
function hashWithGlobalSalt(password) {
return crypto.createHash('sha256')
.update(GLOBAL_SALT + password)
.digest('hex');
}
// ANFÄLLIG: PHP Passwort-Hashing ohne Salt
<?php
function hashPasswordVulnerable($password) {
// Kein Salt - Rainbow-Table-Angriff möglich!
return hash('sha256', $password);
}
// ANFÄLLIG: MD5 - extrem schwach
function hashPasswordMD5($password) {
return md5($password); // Niemals verwenden!
}
// ANFÄLLIG: Statischer Salt
define('APP_SALT', 'static_application_salt');
function hashWithStaticSalt($password) {
return hash('sha256', APP_SALT . $password);
}
?>
Behobener Code
# SICHER: Verwendung von bcrypt (empfohlen)
import bcrypt
def hash_password_bcrypt(password):
# bcrypt generiert und speichert Salt automatisch
# Arbeitsfaktor (rounds) kann für Sicherheit angepasst werden
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password.encode(), salt)
return hashed.decode()
def verify_password_bcrypt(password, stored_hash):
return bcrypt.checkpw(password.encode(), stored_hash.encode())
# SICHER: Verwendung von Argon2 (Gewinner des Password Hashing Competition)
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(
time_cost=3, # Anzahl der Iterationen
memory_cost=65536, # Speicherverbrauch in KB
parallelism=4 # Anzahl paralleler Threads
)
def hash_password_argon2(password):
# Argon2 behandelt Salting automatisch
return ph.hash(password)
def verify_password_argon2(password, stored_hash):
try:
ph.verify(stored_hash, password)
return True
except VerifyMismatchError:
return False
# SICHER: Verwendung von PBKDF2 mit ordentlichem Salt
import hashlib
import os
def hash_password_pbkdf2(password):
# Zufälligen Salt generieren
salt = os.urandom(32)
# Hohe Iterationszahl verwenden
iterations = 600000 # OWASP-Empfehlung für SHA-256
# Schlüssel ableiten
key = hashlib.pbkdf2_hmac(
'sha256',
password.encode(),
salt,
iterations
)
# Salt und Hash zusammen speichern
return salt.hex() + ':' + key.hex()
def verify_password_pbkdf2(password, stored_hash):
salt_hex, key_hex = stored_hash.split(':')
salt = bytes.fromhex(salt_hex)
# Schlüssel mit gleichen Parametern ableiten
key = hashlib.pbkdf2_hmac(
'sha256',
password.encode(),
salt,
600000
)
return key.hex() == key_hex
# SICHER: scrypt für speicherintensives Hashing
import hashlib
def hash_password_scrypt(password):
salt = os.urandom(32)
key = hashlib.scrypt(
password.encode(),
salt=salt,
n=2**14, # CPU/Speicher-Kosten
r=8, # Blockgröße
p=1 # Parallelisierung
)
return salt.hex() + ':' + key.hex()
// SICHER: Java Passwort-Hashing mit bcrypt
import org.mindrot.jbcrypt.BCrypt;
public class SecurePasswordHash {
// Arbeitsfaktor (log rounds) - 12 ist guter Standard
private static final int WORK_FACTOR = 12;
public String hashPassword(String password) {
// BCrypt generiert Salt automatisch
return BCrypt.hashpw(password, BCrypt.gensalt(WORK_FACTOR));
}
public boolean verifyPassword(String password, String storedHash) {
return BCrypt.checkpw(password, storedHash);
}
}
// SICHER: Verwendung von Argon2 in Java
import de.mkammerer.argon2.Argon2;
import de.mkammerer.argon2.Argon2Factory;
public class Argon2PasswordHash {
private final Argon2 argon2 = Argon2Factory.create(
Argon2Factory.Argon2Types.ARGON2id
);
public String hashPassword(String password) {
// Parameter: Iterationen, Speicher, Parallelismus
return argon2.hash(3, 65536, 4, password.toCharArray());
}
public boolean verifyPassword(String password, String storedHash) {
return argon2.verify(storedHash, password.toCharArray());
}
}
// SICHER: Verwendung von PBKDF2 mit ordentlichem Salt
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
public class PBKDF2PasswordHash {
private static final int ITERATIONS = 600000;
private static final int KEY_LENGTH = 256;
private static final int SALT_LENGTH = 32;
public String hashPassword(String password) throws Exception {
// Zufälligen Salt generieren
SecureRandom random = new SecureRandom();
byte[] salt = new byte[SALT_LENGTH];
random.nextBytes(salt);
// Schlüssel ableiten
PBEKeySpec spec = new PBEKeySpec(
password.toCharArray(),
salt,
ITERATIONS,
KEY_LENGTH
);
SecretKeyFactory factory =
SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] hash = factory.generateSecret(spec).getEncoded();
// Salt und Hash kombinieren
String saltBase64 = Base64.getEncoder().encodeToString(salt);
String hashBase64 = Base64.getEncoder().encodeToString(hash);
return saltBase64 + ":" + hashBase64;
}
public boolean verifyPassword(String password, String storedHash) throws Exception {
String[] parts = storedHash.split(":");
byte[] salt = Base64.getDecoder().decode(parts[0]);
PBEKeySpec spec = new PBEKeySpec(
password.toCharArray(),
salt,
ITERATIONS,
KEY_LENGTH
);
SecretKeyFactory factory =
SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] hash = factory.generateSecret(spec).getEncoded();
String hashBase64 = Base64.getEncoder().encodeToString(hash);
return hashBase64.equals(parts[1]);
}
}
// SICHER: Node.js mit bcrypt
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12;
async function hashPassword(password) {
// bcrypt generiert Salt automatisch
return await bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(password, storedHash) {
return await bcrypt.compare(password, storedHash);
}
// SICHER: Verwendung von Argon2
const argon2 = require('argon2');
async function hashPasswordArgon2(password) {
return await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536,
timeCost: 3,
parallelism: 4
});
}
async function verifyPasswordArgon2(password, storedHash) {
return await argon2.verify(storedHash, password);
}
// SICHER: Verwendung von scrypt (in Node.js eingebaut)
const crypto = require('crypto');
const { promisify } = require('util');
const scryptAsync = promisify(crypto.scrypt);
async function hashPasswordScrypt(password) {
const salt = crypto.randomBytes(32);
const derivedKey = await scryptAsync(password, salt, 64, {
N: 16384, // CPU/Speicher-Kosten
r: 8, // Blockgröße
p: 1 // Parallelisierung
});
return `${salt.toString('hex')}:${derivedKey.toString('hex')}`;
}
async function verifyPasswordScrypt(password, storedHash) {
const [saltHex, keyHex] = storedHash.split(':');
const salt = Buffer.from(saltHex, 'hex');
const storedKey = Buffer.from(keyHex, 'hex');
const derivedKey = await scryptAsync(password, salt, 64, {
N: 16384,
r: 8,
p: 1
});
return crypto.timingSafeEqual(derivedKey, storedKey);
}
// SICHER: PHP Passwort-Hashing (eingebaute Funktionen verwenden!)
<?php
function hashPasswordSecure($password) {
// password_hash generiert Salt automatisch und verwendet bcrypt
return password_hash($password, PASSWORD_DEFAULT, [
'cost' => 12 // Arbeitsfaktor
]);
}
function verifyPasswordSecure($password, $storedHash) {
return password_verify($password, $storedHash);
}
// Prüfen ob Passwort Rehashing benötigt (z.B. Arbeitsfaktor erhöht)
function needsRehash($storedHash) {
return password_needs_rehash($storedHash, PASSWORD_DEFAULT, [
'cost' => 12
]);
}
// SICHER: Verwendung von Argon2 (PHP 7.2+)
function hashPasswordArgon2($password) {
return password_hash($password, PASSWORD_ARGON2ID, [
'memory_cost' => 65536,
'time_cost' => 4,
'threads' => 3
]);
}
// Vollständiges Beispiel mit Rehashing
function authenticateUser($password, $storedHash) {
if (!password_verify($password, $storedHash)) {
return false;
}
// Rehash wenn Algorithmus/Kosten geändert
if (password_needs_rehash($storedHash, PASSWORD_ARGON2ID)) {
$newHash = password_hash($password, PASSWORD_ARGON2ID);
// Hash in Datenbank aktualisieren
updateUserPasswordHash($newHash);
}
return true;
}
?>
Ausgenutzt in der Praxis
LinkedIn-Breach (2012)
6,5 Millionen nicht gesalzene SHA-1-Passwort-Hashes wurden geleakt. Innerhalb von Stunden knackten Sicherheitsforscher über 60% davon mit Rainbow Tables.
Adobe-Breach (2013)
153 Millionen Benutzerkonten wurden mit Passwörtern offengelegt, die mit 3DES verschlüsselt waren (nicht einmal gehasht). Der Mangel an Salting machte Musteranalyse trivial - identische Passwörter hatten identische Chiffretexte.
Dropbox-Breach (2012/2016)
68 Millionen Zugangsdaten wurden offengelegt. Während Dropbox bcrypt für neuere Konten verwendete, hatten ältere Konten nicht gesalzene SHA-1-Hashes.
Tools zum Testen/Ausnutzen
-
Hashcat - Passwort-Cracking mit Rainbow Tables.
-
John the Ripper - Passwort-Cracker.
-
CrackStation - Online Rainbow-Table-Suche.
-
Have I Been Pwned - Prüfung auf Passwort-Exposition.
CVE-Beispiele
-
CVE-2019-10790 - Nicht gesalzene Passwortspeicherung.
-
CVE-2021-22893 - Schwaches Passwort-Hashing.
-
CVE-2020-5735 - MD5 Passwort-Hashing-Schwachstelle.
Referenzen
-
MITRE. "CWE-759: Use of a One-Way Hash without a Salt." https://cwe.mitre.org/data/definitions/759.html
-
OWASP. "Password Storage Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html