Verwendung von Passwort-Hash anstatt Passwort für Authentifizierung

Beschreibung

Verwendung von Passwort-Hash anstatt Passwort für Authentifizierung ist eine Authentifizierungs-Schwäche, bei der ein System Passwort-Hashes direkt von Clients zur Authentifizierung akzeptiert und diese mit gespeicherten Hashes vergleicht, anstatt das Klartext-Passwort zu empfangen und es serverseitig zu hashen. Obwohl dieser Ansatz die Sicherheit zu verbessern scheint, indem Passwort-Übertragung vermieden und Server-Last reduziert wird, erzeugt er tatsächlich eine kritische Schwachstelle. Angreifer, die Passwort-Hashes durch irgendwelche Mittel (SQL-Injection, Datenlecks, Speicher-Dumps) erhalten, können die gestohlenen Hashes direkt wiedergeben, um sich zu authentifizieren, ohne jemals die ursprünglichen Passwörter zu kennen.

Risiko

Dieses Design bricht grundlegend das Sicherheitsmodell des Passwort-Hashings. Der gesamte Zweck der Speicherung von Passwort-Hashes ist es, Angreifern, die die Passwort-Datenbank kompromittieren, zu verhindern, sofortigen Zugang zu erhalten - sie sollten zuerst die Hashes knacken müssen. Bei clientseitigem Hashing wird der Hash selbst zum Anmeldedaten. Pass-the-Hash-Angriffe ermöglichen Angreifern die Authentifizierung mit erfassten Hashes. SQL-Injection, die Hashes extrahiert, bietet sofortigen Kontozugang. Memory-Scraping auf dem Server liefert authentifizierbare Anmeldedaten. Netzwerkabfangen von Hashes (selbst über verschlüsselte Kanäle) ermöglicht Replay-Angriffe. Der serverseitige Hash-Vergleich bietet keine zusätzliche Sicherheit gegenüber der Speicherung von Klartext-Passwörtern.

Lösung

Führen Sie Passwort-Hashing immer serverseitig durch. Übertragen Sie Klartext-Passwörter vom Client (über TLS/HTTPS) und hashen Sie sie auf dem Server zum Vergleich. Verwenden Sie geeignete Passwort-Hashing-Algorithmen (bcrypt, Argon2, scrypt) mit einzigartigen Salts pro Passwort. Wenn Netzwerkübertragung ein Problem ist, verwenden Sie TLS/HTTPS - nicht clientseitiges Hashing. Erwägen Sie zusätzliche Authentifizierungsfaktoren (MFA) für sensible Konten. Implementieren Sie Überwachung auf Anmeldedaten-Missbrauchsmuster. Speichern Sie nur den Hash; speichern oder übertragen Sie niemals Klartext-Passwörter. Wenn Zero-Knowledge-Authentifizierung wirklich benötigt wird, verwenden Sie zweckgebundene Protokolle wie SRP (Secure Remote Password).

Häufige Auswirkungen

AuswirkungDetails
ZugriffskontrolleBereich: Zugriffskontrolle

Schutzmechanismus umgehen - Angreifer können sich mit erfassten Hashes authentifizieren, ohne sie zu knacken, wodurch der beabsichtigte Schutz des Passwort-Hashings umgangen wird.
ZugriffskontrolleBereich: Zugriffskontrolle

Privilegien erlangen oder Identität annehmen - Hash-Replay ermöglicht Angreifern, jeden Benutzer zu imitieren, dessen Hash sie besitzen.
VertraulichkeitBereich: Vertraulichkeit

Anwendungsdaten lesen - Authentifizierter Zugriff ermöglicht Angreifern den Zugang zu Benutzerdaten und Systemressourcen.

Beispielcode

Anfälliger Code

// Anfällig: Clientseitiges Hashing
// Client-Code
async function login(username, password) {
    // Anfällig: Passwort wird auf Client gehasht
    const passwordHash = await crypto.subtle.digest(
        'SHA-256',
        new TextEncoder().encode(password)
    );
    const hashHex = Array.from(new Uint8Array(passwordHash))
        .map(b => b.toString(16).padStart(2, '0'))
        .join('');

    // Hash wird an Server gesendet
    const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ username, passwordHash: hashHex })
    });
    return response.json();
}

// Server-Code
app.post('/api/login', (req, res) => {
    const { username, passwordHash } = req.body;

    // Anfällig: Server vergleicht nur Hashes
    const user = users.findByUsername(username);
    if (user && user.storedHash === passwordHash) {
        // Angreifer mit gestohlenem Hash kann sich direkt authentifizieren!
        createSession(user);
        res.json({ success: true });
    } else {
        res.status(401).json({ error: 'Ungültige Anmeldedaten' });
    }
});
# Anfällig: Hash-basierte Authentifizierung
import hashlib

class VulnerableAuthSystem:

    def authenticate(self, username, password_hash):
        """
        Anfällig: Akzeptiert Hash direkt vom Client
        """
        user = self.get_user(username)
        if user is None:
            return False

        # Anfällig: Direkter Hash-Vergleich
        # Angreifer, der Hash stiehlt, kann ihn wiedergeben
        if user['password_hash'] == password_hash:
            return True
        return False
// Anfällig: Client sendet vorberechneten Hash
public class VulnerableAuthService {

    public boolean authenticate(String username, String clientHash) {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            return false;
        }

        // Anfällig: Vergleich von client-bereitgestelltem Hash mit gespeichertem Hash
        // Wenn Angreifer den Hash hat, kann er sich direkt einloggen
        return user.getPasswordHash().equals(clientHash);
    }
}
// Anfällig: Hash-Vergleichs-Authentifizierung
<?php
function vulnerable_login($username, $client_hash) {
    $user = get_user($username);
    if (!$user) {
        return false;
    }

    // Anfällig: Hash ist die Anmeldedaten
    if ($user['password_hash'] === $client_hash) {
        // SQL-Injection, die Hashes enthüllt = sofortige Kontokompromittierung
        create_session($user);
        return true;
    }
    return false;
}
?>
// Anfällig: Netzwerkprotokoll mit Hash-Authentifizierung
int vulnerable_auth(connection_t *conn) {
    char username[64];
    char client_hash[65];  // SHA-256 hex

    recv(conn->socket, username, sizeof(username), 0);
    recv(conn->socket, client_hash, sizeof(client_hash), 0);

    user_t *user = lookup_user(username);
    if (!user) {
        return AUTH_FAILED;
    }

    // Anfällig: Hash-Vergleich - abgefangener Hash kann wiedergegeben werden
    if (strcmp(user->password_hash, client_hash) == 0) {
        return AUTH_SUCCESS;
    }

    return AUTH_FAILED;
}

Korrigierter Code

// Korrigiert: Serverseitiges Hashing
// Client-Code - sendet Klartext über HTTPS
async function login(username, password) {
    // Korrigiert: Klartext-Passwort senden (über HTTPS)
    const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password })
    });
    return response.json();
}

// Server-Code - hasht serverseitig
const bcrypt = require('bcrypt');

app.post('/api/login', async (req, res) => {
    const { username, password } = req.body;

    const user = await users.findByUsername(username);
    if (!user) {
        // Konstante Zeitverzögerung um Timing-Angriffe zu verhindern
        await bcrypt.compare(password, '$2b$10$dummy.hash.for.timing');
        return res.status(401).json({ error: 'Ungültige Anmeldedaten' });
    }

    // Korrigiert: Server hasht und vergleicht
    // Gestohlener Hash kann nicht direkt verwendet werden - Angreifer muss ihn knacken
    const valid = await bcrypt.compare(password, user.passwordHash);
    if (valid) {
        createSession(user);
        res.json({ success: true });
    } else {
        res.status(401).json({ error: 'Ungültige Anmeldedaten' });
    }
});
# Korrigiert: Serverseitiges Passwort-Hashing
import bcrypt

class FixedAuthSystem:

    def authenticate(self, username, password):
        """
        Korrigiert: Passwort wird serverseitig gehasht
        """
        user = self.get_user(username)
        if user is None:
            # Konstant-Zeit-Vergleich um Timing-Angriffe zu verhindern
            bcrypt.checkpw(b'dummy', b'$2b$12$dummy.hash.for.timing.attack.prevention')
            return False

        # Korrigiert: Server hasht das Passwort und vergleicht
        # Selbst wenn Angreifer Hash stiehlt, muss er ihn zuerst knacken
        password_bytes = password.encode('utf-8')
        stored_hash = user['password_hash'].encode('utf-8')

        return bcrypt.checkpw(password_bytes, stored_hash)

    def set_password(self, user_id, password):
        """Ordnungsgemäß gehashtes Passwort speichern"""
        password_bytes = password.encode('utf-8')
        # Korrigiert: bcrypt mit gutem Kostenfaktor verwenden
        hashed = bcrypt.hashpw(password_bytes, bcrypt.gensalt(rounds=12))
        self.update_user_hash(user_id, hashed.decode('utf-8'))
// Korrigiert: Serverseitige Passwort-Verifikation
import org.mindrot.jbcrypt.BCrypt;

public class FixedAuthService {

    public boolean authenticate(String username, String password) {
        User user = userRepository.findByUsername(username);

        if (user == null) {
            // Timing-Angriffe verhindern
            BCrypt.checkpw(password, "$2a$12$dummy.hash.for.timing");
            return false;
        }

        // Korrigiert: Server verifiziert Passwort gegen gespeicherten Hash
        // Gestohlener Hash erfordert Knacken vor Verwendung
        return BCrypt.checkpw(password, user.getPasswordHash());
    }

    public void setPassword(User user, String password) {
        // Korrigiert: Passwort serverseitig vor Speicherung hashen
        String hash = BCrypt.hashpw(password, BCrypt.gensalt(12));
        user.setPasswordHash(hash);
        userRepository.save(user);
    }
}
// Korrigiert: Serverseitiges Passwort-Hashing mit PHP
<?php
function fixed_login($username, $password) {
    $user = get_user($username);

    if (!$user) {
        // Timing-Angriff-Prävention
        password_verify($password, '$2y$12$dummyhashfortimingattack');
        return false;
    }

    // Korrigiert: password_verify verwenden - serverseitiger Vergleich
    if (password_verify($password, $user['password_hash'])) {
        create_session($user);
        return true;
    }
    return false;
}

function set_password($user_id, $password) {
    // Korrigiert: Passwort serverseitig hashen
    $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
    update_user_password($user_id, $hash);
}
?>
// Korrigiert: Ordnungsgemäße Authentifizierung mit serverseitigem Hashing
import (
    "golang.org/x/crypto/bcrypt"
)

func authenticate(username, password string) bool {
    user, err := getUserByUsername(username)
    if err != nil || user == nil {
        // Timing-Angriff-Prävention
        bcrypt.CompareHashAndPassword(
            []byte("$2a$12$dummyhashfortiming"),
            []byte(password))
        return false
    }

    // Korrigiert: Server vergleicht Passwort gegen gespeicherten Hash
    err = bcrypt.CompareHashAndPassword(
        []byte(user.PasswordHash),
        []byte(password))

    return err == nil
}

func setPassword(userID int, password string) error {
    // Korrigiert: Serverseitig mit angemessenen Kosten hashen
    hash, err := bcrypt.GenerateFromPassword(
        []byte(password),
        bcrypt.DefaultCost)
    if err != nil {
        return err
    }

    return updateUserPassword(userID, string(hash))
}

Verwandte CWEs

  • CWE-1390: Schwache Authentifizierung (Eltern)
  • CWE-602: Clientseitige Durchsetzung serverseitiger Sicherheit (verwandt)
  • CWE-287: Unzureichende Authentifizierung (verwandt)
  • CWE-916: Verwendung von Passwort-Hash mit unzureichendem Rechenaufwand (verwandt)

Referenzen

  1. MITRE Corporation. "CWE-836: Use of Password Hash Instead of Password for Authentication." https://cwe.mitre.org/data/definitions/836.html
  2. CAPEC-644. "Use of Captured Hashes (Pass The Hash)."
  3. OWASP. "Password Storage Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html