Speicherung von Passwörtern in wiederherstellbarem Format

Beschreibung

Speicherung von Passwörtern in wiederherstellbarem Format tritt auf, wenn Software Passwörter auf eine Weise speichert, die es ermöglicht, sie in ihre ursprüngliche Klartextform wiederherzustellen. Dies umfasst die Verwendung reversibler Verschlüsselung, Kodierung (Base64), XOR-Verschleierung oder benutzerdefinierter "Verschlüsselungs"-Schemata. Ordnungsgemäße Passwortspeicherung verwendet kryptografische Einweg-Hash-Funktionen, die die Passwortwiederherstellung rechnerisch unmöglich machen. Jedes wiederherstellbare Speicherformat bedeutet, dass Angreifer, die Zugang zum Speicher erhalten, alle Passwörter abrufen können.

Risiko

Wiederherstellbare Passwortspeicherung legt alle Benutzer offen, wenn das System kompromittiert wird. Anders als bei ordnungsgemäßem Hashing, wo Angreifer jedes Passwort einzeln knacken müssen, ermöglicht reversible Verschlüsselung oder Kodierung sofortige Massenwiederherstellung. Der Verschlüsselungsschlüssel oder Algorithmus wird zum einzigen Fehlerpunkt—wenn entdeckt, sind alle Passwörter sofort kompromittiert. Dies hat zu massiven Breaches geführt, bei denen Millionen von Passwörtern in Minuten wiederhergestellt wurden. Selbst "verschlüsselte" Passwörter mit symmetrischer Verschlüsselung bieten falsche Sicherheit.

Lösung

Verwenden Sie immer ordnungsgemäße Passwort-Hashing-Algorithmen, die für Passwortspeicherung konzipiert sind: bcrypt, scrypt oder Argon2. Verwenden Sie niemals Verschlüsselung für Passwortspeicherung—Passwörter müssen nicht wiederhergestellt werden, nur verifiziert. Verwenden Sie eindeutige Salts pro Passwort. Konfigurieren Sie angemessene Arbeitsfaktoren. Wenn Legacy-Systeme wiederherstellbare Speicherung verwenden, migrieren Sie sofort zu ordnungsgemäßem Hashing. Für Systeme, die Passwortwiederherstellung erfordern, implementieren Sie sichere Passwort-Reset-Flows anstatt wiederherstellbare Passwörter zu speichern.

Häufige Auswirkungen

AuswirkungDetails
VertraulichkeitBereich: Massen-Passwort-Offenlegung

Alle Passwörter können gleichzeitig wiederhergestellt werden, sobald der Speicher kompromittiert ist.
AuthentifizierungBereich: Kontoübernahme

Wiederhergestellte Passwörter ermöglichen sofortigen Zugang zu allen Benutzerkonten.
DatenschutzBereich: Cross-Site-Kompromittierung

Benutzer, die Passwörter wiederverwenden, werden auf mehreren Sites kompromittiert.

Beispielcode + Lösungscode

Anfälliger Code

# ANFÄLLIG: Base64-"Verschlüsselung" (nur Kodierung!)
import base64

def store_password_base64(password):
    # Das ist KEINE Verschlüsselung - leicht reversibel!
    encoded = base64.b64encode(password.encode()).decode()
    return encoded

def verify_password_base64(password, stored):
    decoded = base64.b64decode(stored).decode()
    return password == decoded  # Passwort wird wiederhergestellt!

# ANFÄLLIG: Symmetrische Verschlüsselung für Passwörter
from cryptography.fernet import Fernet

# Wenn dieser Schlüssel kompromittiert wird, sind ALLE Passwörter offengelegt
ENCRYPTION_KEY = Fernet.generate_key()

def store_password_encrypted(password):
    cipher = Fernet(ENCRYPTION_KEY)
    return cipher.encrypt(password.encode())

def verify_password_encrypted(password, stored):
    cipher = Fernet(ENCRYPTION_KEY)
    decrypted = cipher.decrypt(stored).decode()  # Passwort wiederhergestellt!
    return password == decrypted

# ANFÄLLIG: XOR-"Verschlüsselung"
def xor_encrypt(password, key="secret"):
    return ''.join(chr(ord(p) ^ ord(k)) for p, k in zip(password, key * len(password)))

def store_password_xor(password):
    return xor_encrypt(password)  # Trivial reversibel!

# ANFÄLLIG: Benutzerdefinierte "Verschlüsselung"
def custom_encrypt(password):
    # ROT13 oder ähnlich - keine echte Verschlüsselung!
    return password.translate(str.maketrans(
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
        'NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm'
    ))
// ANFÄLLIG: AES-Verschlüsselung für Passwörter
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class VulnerablePasswordStorage {

    private static final byte[] ENCRYPTION_KEY = "MySecretKey12345".getBytes();

    public String encryptPassword(String password) throws Exception {
        SecretKeySpec keySpec = new SecretKeySpec(ENCRYPTION_KEY, "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, keySpec);

        byte[] encrypted = cipher.doFinal(password.getBytes());
        return Base64.getEncoder().encodeToString(encrypted);
    }

    public boolean verifyPassword(String password, String stored) throws Exception {
        SecretKeySpec keySpec = new SecretKeySpec(ENCRYPTION_KEY, "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.DECRYPT_MODE, keySpec);

        byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(stored));
        String originalPassword = new String(decrypted);  // Passwort wiederhergestellt!

        return password.equals(originalPassword);
    }
}

// ANFÄLLIG: Verschleierung mit Verschlüsselung verwechselt
public class ObfuscatedPassword {

    public String obfuscate(String password) {
        // Nur Hex-Kodierung - trivial reversibel!
        StringBuilder hex = new StringBuilder();
        for (char c : password.toCharArray()) {
            hex.append(String.format("%02x", (int) c));
        }
        return hex.toString();
    }

    public String deobfuscate(String hex) {
        StringBuilder password = new StringBuilder();
        for (int i = 0; i < hex.length(); i += 2) {
            password.append((char) Integer.parseInt(hex.substring(i, i + 2), 16));
        }
        return password.toString();  // Original-Passwort!
    }
}
// ANFÄLLIG: Base64-Kodierung
function storePasswordBase64(password) {
    // KEINE Verschlüsselung!
    return Buffer.from(password).toString('base64');
}

function verifyPasswordBase64(password, stored) {
    const decoded = Buffer.from(stored, 'base64').toString();
    return password === decoded;  // Passwort wiederhergestellt!
}

// ANFÄLLIG: Crypto mit reversibler Verschlüsselung
const crypto = require('crypto');

const ALGORITHM = 'aes-256-cbc';
const KEY = crypto.randomBytes(32);
const IV = crypto.randomBytes(16);

function encryptPassword(password) {
    const cipher = crypto.createCipheriv(ALGORITHM, KEY, IV);
    let encrypted = cipher.update(password, 'utf8', 'hex');
    encrypted += cipher.final('hex');
    return encrypted;
}

function decryptPassword(encrypted) {
    const decipher = crypto.createDecipheriv(ALGORITHM, KEY, IV);
    let decrypted = decipher.update(encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');
    return decrypted;  // Original-Passwort!
}

// ANFÄLLIG: Einfache Substitution
function simpleEncrypt(password) {
    return password.split('').map(c =>
        String.fromCharCode(c.charCodeAt(0) + 1)
    ).join('');  // Verschiebt nur Zeichen - trivial reversibel!
}

Korrigierter Code

# SICHER: bcrypt verwenden (empfohlen)
import bcrypt

def hash_password_bcrypt(password):
    """Passwort mit bcrypt hashen - NICHT wiederherstellbar."""
    salt = bcrypt.gensalt(rounds=12)
    hashed = bcrypt.hashpw(password.encode(), salt)
    return hashed.decode()

def verify_password_bcrypt(password, stored_hash):
    """Passwort verifizieren ohne es wiederherzustellen."""
    return bcrypt.checkpw(password.encode(), stored_hash.encode())

# SICHER: Argon2 verwenden (Gewinner des Password Hashing Competition)
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

ph = PasswordHasher(
    time_cost=3,
    memory_cost=65536,
    parallelism=4
)

def hash_password_argon2(password):
    """Passwort mit Argon2 hashen - NICHT wiederherstellbar."""
    return ph.hash(password)

def verify_password_argon2(password, stored_hash):
    """Passwort verifizieren ohne es wiederherzustellen."""
    try:
        ph.verify(stored_hash, password)
        return True
    except VerifyMismatchError:
        return False

# SICHER: PBKDF2 mit hohen Iterationen verwenden
import hashlib
import os

def hash_password_pbkdf2(password):
    """Passwort mit PBKDF2 hashen - NICHT wiederherstellbar."""
    salt = os.urandom(32)
    iterations = 600000  # OWASP-Empfehlung für SHA-256

    key = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, iterations)

    return f"pbkdf2:sha256:{iterations}:{salt.hex()}:{key.hex()}"

def verify_password_pbkdf2(password, stored_hash):
    """Passwort verifizieren ohne es wiederherzustellen."""
    parts = stored_hash.split(':')
    iterations = int(parts[2])
    salt = bytes.fromhex(parts[3])
    stored_key = bytes.fromhex(parts[4])

    computed_key = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, iterations)

    # Konstantzeit-Vergleich
    return hmac.compare_digest(computed_key, stored_key)

# Migration von wiederherstellbarer Speicherung
def migrate_password(user, old_password_encrypted, encryption_key):
    """Von verschlüsselter zu gehashter Speicherung migrieren."""
    # Altes Passwort entschlüsseln (einmalige Operation)
    cipher = Fernet(encryption_key)
    plaintext = cipher.decrypt(old_password_encrypted).decode()

    # Mit bcrypt hashen
    hashed = hash_password_bcrypt(plaintext)

    # Klartext aus Speicher löschen
    del plaintext

    # Benutzerdatensatz aktualisieren
    user.password_hash = hashed
    user.password_encrypted = None  # Altes Feld entfernen
    user.save()
// SICHER: BCrypt in Java verwenden
import org.mindrot.jbcrypt.BCrypt;

public class SecurePasswordStorage {

    private static final int WORK_FACTOR = 12;

    public String hashPassword(String password) {
        // Generiert automatisch eindeutigen Salt
        // Ergebnis ist NICHT wiederherstellbar!
        return BCrypt.hashpw(password, BCrypt.gensalt(WORK_FACTOR));
    }

    public boolean verifyPassword(String password, String storedHash) {
        // Verifiziert ohne Passwort wiederherzustellen
        return BCrypt.checkpw(password, storedHash);
    }
}

// SICHER: Argon2 in Java verwenden
import de.mkammerer.argon2.Argon2;
import de.mkammerer.argon2.Argon2Factory;

public class Argon2PasswordStorage {

    private final Argon2 argon2;

    public Argon2PasswordStorage() {
        this.argon2 = Argon2Factory.create(Argon2Factory.Argon2Types.ARGON2id);
    }

    public String hashPassword(String password) {
        // Hash ist NICHT wiederherstellbar
        return argon2.hash(3, 65536, 4, password.toCharArray());
    }

    public boolean verifyPassword(String password, String storedHash) {
        try {
            return argon2.verify(storedHash, password.toCharArray());
        } finally {
            // Sensible Daten löschen
            argon2.wipeArray(password.toCharArray());
        }
    }

    public boolean needsRehash(String storedHash) {
        return argon2.needsRehash(storedHash, 3, 65536, 4);
    }
}

// SICHER: Spring Security Passwort-Kodierung
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
}

@Service
public class UserService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    public void createUser(String username, String password) {
        User user = new User();
        user.setUsername(username);
        // Hash ist NICHT wiederherstellbar
        user.setPasswordHash(passwordEncoder.encode(password));
        userRepository.save(user);
    }

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

        // Verifizieren ohne wiederherzustellen
        return passwordEncoder.matches(password, user.getPasswordHash());
    }
}
// SICHER: bcrypt in Node.js verwenden
const bcrypt = require('bcrypt');

const SALT_ROUNDS = 12;

async function hashPassword(password) {
    // Hash ist NICHT wiederherstellbar
    return await bcrypt.hash(password, SALT_ROUNDS);
}

async function verifyPassword(password, storedHash) {
    // Verifizieren ohne wiederherzustellen
    return await bcrypt.compare(password, storedHash);
}

// SICHER: Argon2 in Node.js verwenden
const argon2 = require('argon2');

async function hashPasswordArgon2(password) {
    // Hash ist NICHT wiederherstellbar
    return await argon2.hash(password, {
        type: argon2.argon2id,
        memoryCost: 65536,
        timeCost: 3,
        parallelism: 4
    });
}

async function verifyPasswordArgon2(password, storedHash) {
    // Verifizieren ohne wiederherzustellen
    return await argon2.verify(storedHash, password);
}

// SICHER: scrypt verwenden (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 key = await scryptAsync(password, salt, 64, {
        N: 16384, r: 8, p: 1
    });

    // Hash ist NICHT wiederherstellbar
    return `${salt.toString('hex')}:${key.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 computedKey = await scryptAsync(password, salt, 64, {
        N: 16384, r: 8, p: 1
    });

    // Konstantzeit-Vergleich
    return crypto.timingSafeEqual(computedKey, storedKey);
}

// Migrationsbeispiel
async function migrateFromEncrypted(user, decryptionKey) {
    // Einmalige Entschlüsselung für Migration
    const cipher = crypto.createDecipheriv('aes-256-cbc', decryptionKey, iv);
    let plaintext = cipher.update(user.encryptedPassword, 'hex', 'utf8');
    plaintext += cipher.final('utf8');

    // Ordnungsgemäß hashen
    user.passwordHash = await hashPassword(plaintext);
    user.encryptedPassword = null;  // Altes Feld entfernen

    await user.save();

    // Klartext löschen
    plaintext = null;
}

Ausgenutzt in der Praxis

Adobe Breach (2013)

153 Millionen Benutzerpasswörter wurden mit 3DES-Verschlüsselung offengelegt. Da derselbe Schlüssel alle Passwörter verschlüsselte, produzierten identische Passwörter identische Geheimtexte, was Musteranalyse trivial machte.

Ashley Madison (2015)

Während bcrypt für einige Passwörter verwendet wurde, hatten ältere Konten MD5-Hashes, die schnell geknackt wurden und Millionen von Benutzern offenlegten.

RockYou (2009)

32 Millionen im Klartext gespeicherte Passwörter wurden gebreacht und wurden zu einer Eckpfeiler-Wortliste für Passwort-Cracking.


Tools zum Testen/Ausnutzen

  • Hashcat — Passwort-Hashes identifizieren und knacken.

  • John the Ripper — Passwortwiederherstellung.

  • CyberChef — Base64, Hex und andere Kodierungen dekodieren.


CVE-Beispiele


Referenzen

  1. MITRE. "CWE-257: Storing Passwords in a Recoverable Format." https://cwe.mitre.org/data/definitions/257.html

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