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
| Auswirkung | Details |
|---|---|
| Zugriffskontrolle | Bereich: Zugriffskontrolle Schutzmechanismus umgehen - Angreifer können sich mit erfassten Hashes authentifizieren, ohne sie zu knacken, wodurch der beabsichtigte Schutz des Passwort-Hashings umgangen wird. |
| Zugriffskontrolle | Bereich: Zugriffskontrolle Privilegien erlangen oder Identität annehmen - Hash-Replay ermöglicht Angreifern, jeden Benutzer zu imitieren, dessen Hash sie besitzen. |
| Vertraulichkeit | Bereich: 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
- MITRE Corporation. "CWE-836: Use of Password Hash Instead of Password for Authentication." https://cwe.mitre.org/data/definitions/836.html
- CAPEC-644. "Use of Captured Hashes (Pass The Hash)."
- OWASP. "Password Storage Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html