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

AuswirkungDetails
VertraulichkeitBereich: Passwort-Offenlegung

Rainbow-Table-Angriffe können nicht gesalzene Hashes für häufige Passwörter in Sekunden knacken.
AuthentifizierungBereich: Kontoübernahme

Geknackte Passwörter führen zu unbefugtem Kontozugriff.
PrivatsphäreBereich: 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


CVE-Beispiele


Referenzen

  1. MITRE. "CWE-759: Use of a One-Way Hash without a Salt." https://cwe.mitre.org/data/definitions/759.html

  2. OWASP. "Password Storage Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html