Authentifizierungsumgehung durch primäre Schwäche

Beschreibung

Authentifizierungsumgehung durch primäre Schwäche ist eine Schwachstelle, die auftritt, wenn der Authentifizierungsalgorithmus selbst solide ist, aber eine zugrundeliegende Schwäche in anderen Teilen der Implementierung die Umgehung des Authentifizierungsmechanismus ermöglicht. Dies wird als resultierende Schwäche klassifiziert -- die Authentifizierungsumgehung beruht nicht auf Fehlern in der Authentifizierungslogik selbst, sondern stammt von separaten Problemen wie unsachgemäßer Eingabeverarbeitung, Speicherverwaltungsfehlern oder Vergleichslogikfehlern. Häufige Beispiele umfassen Passwortvergleiche, die nur das erste Zeichen prüfen, nicht initialisierte Variablen, die Passwörter abschneiden, oder Pufferbehandlungsfehler, die eine Umgehung ermöglichen.

Risiko

Authentifizierungsumgehung durch primäre Schwächen ist besonders heimtückisch, da Sicherheitsreviews, die sich ausschließlich auf den Authentifizierungsalgorithmus konzentrieren, die zugrundeliegende Schwachstelle übersehen können. Das Risiko ist schwerwiegend, da diese Schwächen oft komplexe Authentifizierung auf trivial umgehbare Prüfungen reduzieren -- wie die Verifizierung nur eines einzelnen Zeichens eines Passworts. Angreifer, die diese zugrundeliegenden Fehler identifizieren, können die Authentifizierung mit minimalem Aufwand umgehen. Die Schwachstellen, die diese Umgehungen verursachen, umfassen mehrere Kategorien einschließlich Speichersicherheitsprobleme, Vergleichsfehler und Initialisierungsprobleme. Da der Authentifizierungsmechanismus selbst korrekt erscheint, können diese Probleme durch mehrere Sicherheitsaudits bestehen bleiben, bis sie durch detaillierte Codeanalyse oder Ausnutzung entdeckt werden.

Lösung

Führen Sie umfassende Sicherheitstests durch, die über die Authentifizierungslogik hinausgehen und den gesamten unterstützenden Code untersuchen. Verwenden Sie speichersichere Programmiersprachen oder wenden Sie strenge Speichersicherheitspraktiken bei Verwendung von Sprachen wie C an. Initialisieren Sie alle Variablen explizit, insbesondere solche, die in sicherheitskritischen Vergleichen verwendet werden. Verwenden Sie Konstante-Zeit-Vergleichsfunktionen für Passwort- und Zugangsdaten-Verifizierung. Implementieren Sie umfassende Unit-Tests, die verifizieren, dass vollständige Zugangsdaten-Werte verglichen werden, nicht nur Präfixe. Wenden Sie statische Analyse-Tools an, um nicht initialisierte Variablen und Vergleichslogikfehler zu erkennen. Validieren Sie Eingabelängen vor der Verarbeitung von Zugangsdaten. Verwenden Sie gut getestete Authentifizierungsbibliotheken anstelle benutzerdefinierter Implementierungen, die subtile Fehler enthalten können.

Häufige Auswirkungen

AuswirkungDetails
ZugriffskontrolleBereich: Zugriffskontrolle

Die zugrundeliegende Schwäche ermöglicht es Angreifern, die Authentifizierung vollständig zu umgehen und unautorisierten Zugang mit minimalen Zugangsdaten oder Aufwand zu erhalten.
Integrität, VertraulichkeitBereich: Integrität, Vertraulichkeit

Bei umgangener Authentifizierung erhalten Angreifer vollen Zugang zu geschützten Ressourcen, können Benutzeridentitäten annehmen und sensible Daten einsehen oder ändern.

Beispielcode und Lösung

Verwundbarer Code (C)

Die folgenden Beispiele zeigen Authentifizierungsumgehung durch zugrundeliegende Schwächen:

// Verwundbar: Passwortvergleich prüft nur erstes Zeichen
#include <string.h>
#include <stdlib.h>

typedef struct {
    char username[64];
    char password[64];
} UserCredentials;

int vulnerable_authenticate(const char *username, const char *password) {
    UserCredentials *user = lookup_user(username);

    if (user == NULL) {
        return 0;
    }

    // Verwundbar: strlen gibt 0 zurück aufgrund nicht initialisierten Speichers
    // oder Pufferberechnungsfehler
    size_t password_len = get_password_length(user);

    // Verwundbar: Vergleicht nur erstes Zeichen wenn password_len 1 ist
    // aufgrund eines Fehlers in get_password_length()
    if (strncmp(password, user->password, password_len) == 0) {
        return 1;  // Mit nur dem ersten Zeichen authentifiziert!
    }

    return 0;
}

// Verwundbar: Nicht initialisiertes Array schneidet Passwort ab
int vulnerable_password_check(const char *input, const char *stored) {
    char buffer[64];
    // Verwundbar: buffer ist nicht initialisiert
    // Erstes Null-Byte in nicht initialisierten Daten schneidet Vergleich ab

    int i;
    for (i = 0; input[i] != '\0' && i < 63; i++) {
        buffer[i] = input[i];
    }
    // Verwundbar: Fehlender Null-Terminator wenn input genau 63 Zeichen lang ist

    // Wenn buffer ein zufälliges Null-Byte an Position 1 hat, wird nur erstes Zeichen verglichen
    return strcmp(buffer, stored) == 0;
}
# Verwundbar: Integer-Behandlungsfehler verursacht teilweisen Vergleich
def vulnerable_verify_password(input_password, stored_hash):
    # Verwundbar: Längenfehlberechnung aufgrund von Kodierungsproblemen
    # UTF-8-Multibyte-Zeichen verursachen Längenunterschied

    input_bytes = input_password.encode('utf-8')
    stored_bytes = stored_hash.encode('utf-8')

    # Verwundbar: Verwendet falsche Länge für Vergleich
    # Wenn Angreifer bestimmtes Unicode eingibt, gibt len() falschen Wert zurück
    compare_length = len(input_password)  # Zeichenanzahl, nicht Byte-Anzahl!

    # Vergleicht nur teilweisen Hash bei Längenunterschied
    if input_bytes[:compare_length] == stored_bytes[:compare_length]:
        return True

    return False

# Verwundbar: Typverwechslung ermöglicht Umgehung
def vulnerable_auth_check(username, password):
    user = get_user(username)
    if user is None:
        return False

    stored_password = user.get('password')

    # Verwundbar: Wenn password-Parameter kein String ist,
    # könnte sich der Vergleich unerwartet verhalten
    if password == stored_password:
        return True

    # Verwundbar: Leerer String-Vergleich
    if not password and not stored_password:
        return True  # Beide "leer" - umgangen!

    return False
// Verwundbar: Null-Byte-Injection schneidet Vergleich ab
public class VulnerableAuth {

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

        byte[] storedPassword = user.getPasswordBytes();

        // Verwundbar: Konvertierung in String, Null-Byte schneidet ab
        String inputStr = new String(password);  // Stoppt beim ersten \0
        String storedStr = new String(storedPassword);

        // Wenn Angreifer "a\0garbage" sendet, wird nur "a" verglichen
        return inputStr.equals(storedStr);
    }

    // Verwundbar: Array-Grenzenproblem verursacht teilweisen Vergleich
    public boolean vulnerableCompare(char[] input, char[] stored) {
        // Verwundbar: Verwendet Eingabelänge, nicht gespeicherte Länge
        // Angreifer sendet kurze Eingabe, um Vergleich zu reduzieren
        for (int i = 0; i < input.length; i++) {
            if (i >= stored.length || input[i] != stored[i]) {
                return false;
            }
        }
        // Gibt true zurück wenn Eingabe Präfix von gespeichertem Wert ist!
        // "a" passt zu "admin123"
        return true;
    }
}

Sichere Lösung (C)

// Behoben: Vollständiger Passwortvergleich mit korrekter Initialisierung
#include <string.h>
#include <stdlib.h>
#include <openssl/crypto.h>

typedef struct {
    char username[64];
    char password_hash[65];  // SHA-256 hex + Null
    size_t password_len;     // Explizit gespeicherte Länge
} UserCredentials;

int secure_authenticate(const char *username, const char *password) {
    UserCredentials *user = lookup_user(username);

    // Konstante-Zeit-Verhalten auch wenn Benutzer nicht gefunden
    if (user == NULL) {
        // Dummy-Hash durchführen um Timing-Angriffe zu verhindern
        char dummy_hash[65];
        compute_password_hash(password, dummy_hash);
        return 0;
    }

    // Hash des Eingabepassworts berechnen
    char input_hash[65];
    compute_password_hash(password, input_hash);

    // Behoben: Konstante-Zeit-Vergleich vollständiger Strings
    // CRYPTO_memcmp gibt 0 bei Gleichheit zurück, sonst ungleich Null
    if (CRYPTO_memcmp(input_hash, user->password_hash, 64) == 0) {
        return 1;
    }

    return 0;
}

// Behoben: Korrekte Pufferinitialisierung und Grenzprüfung
int secure_password_check(const char *input, const char *stored) {
    // Behoben: Puffer explizit mit Nullen initialisieren
    char buffer[64] = {0};

    if (input == NULL || stored == NULL) {
        return 0;
    }

    size_t input_len = strlen(input);
    size_t stored_len = strlen(stored);

    // Behoben: Längen vor Vergleich prüfen
    if (input_len != stored_len) {
        return 0;
    }

    if (input_len >= sizeof(buffer)) {
        return 0;  // Eingabe zu lang
    }

    // Behoben: Mit explizitem Null-Terminator kopieren
    strncpy(buffer, input, sizeof(buffer) - 1);
    buffer[sizeof(buffer) - 1] = '\0';

    // Behoben: Konstante-Zeit-Vergleich vollständiger Strings
    return CRYPTO_memcmp(buffer, stored, stored_len) == 0;
}
# Behoben: Ordnungsgemäßer vollständiger Passwortvergleich
import hmac
import hashlib

def secure_verify_password(input_password, stored_hash):
    if not input_password or not stored_hash:
        return False

    # Behoben: Eingabepasswort mit demselben Algorithmus hashen
    input_hash = hashlib.sha256(input_password.encode('utf-8')).hexdigest()

    # Behoben: Konstante-Zeit-Vergleich vollständiger Hashes
    return hmac.compare_digest(input_hash, stored_hash)

def secure_auth_check(username, password):
    # Behoben: Typen vor Vergleich validieren
    if not isinstance(username, str) or not isinstance(password, str):
        return False

    if not username or not password:
        return False

    user = get_user(username)
    if user is None:
        # Behoben: Dummy-Arbeit durchführen um Timing-Angriffe zu verhindern
        dummy_hash = hashlib.sha256(password.encode()).hexdigest()
        return False

    stored_hash = user.get('password_hash')
    if not stored_hash:
        return False

    # Behoben: Korrekt hashen und vergleichen
    input_hash = hashlib.sha256(password.encode('utf-8')).hexdigest()
    return hmac.compare_digest(input_hash, stored_hash)
// Behoben: Ordnungsgemäßer Byte-Vergleich ohne Null-Byte-Probleme
import java.security.MessageDigest;
import java.util.Arrays;

public class SecureAuth {

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

        // Behoben: Konstante-Zeit-Verhalten
        byte[] storedHash = (user != null) ?
            user.getPasswordHash() : getDummyHash();

        // Behoben: Passwort hashen, nicht in String konvertieren
        byte[] inputHash = hashPassword(password);

        // Behoben: MessageDigest.isEqual für Konstante-Zeit-Vergleich verwenden
        boolean valid = MessageDigest.isEqual(inputHash, storedHash);

        // Sensible Daten löschen
        Arrays.fill(password, (byte) 0);
        Arrays.fill(inputHash, (byte) 0);

        return user != null && valid;
    }

    // Behoben: Ordnungsgemäßer Array-Vergleich
    public boolean secureCompare(char[] input, char[] stored) {
        if (input == null || stored == null) {
            return false;
        }

        // Behoben: Muss exakt gleiche Länge haben
        if (input.length != stored.length) {
            return false;
        }

        // Behoben: ALLE Zeichen vergleichen
        int result = 0;
        for (int i = 0; i < stored.length; i++) {
            result |= input[i] ^ stored[i];
        }

        return result == 0;
    }

    private byte[] hashPassword(byte[] password) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            return md.digest(password);
        } catch (Exception e) {
            throw new RuntimeException("Hashing failed", e);
        }
    }
}

Die Behebung stellt vollständigen Zugangsdatenvergleich mit korrekter Initialisierung und Konstante-Zeit-Operationen sicher.


Ausgenutzt in der Praxis

Einzelzeichen-Passwortvergleich (Verschiedene Systeme, 2000-2002)

CVE-2002-1374 und CVE-2000-0979 dokumentierten Systeme, bei denen die Passwortvalidierung nur das erste Zeichen verglich, was eine Authentifizierungsumgehung mit jedem Passwort ermöglichte, das mit dem korrekten Buchstaben beginnt.

Nicht initialisierter Passwortpuffer (Enterprise-Software, 2001)

CVE-2001-0088 dokumentierte Software, bei der ein nicht initialisiertes Array dazu führte, dass Passwörter effektiv zu einzelnen Zeichen wurden, was triviale Brute-Force-Angriffe ermöglichte.


Tools zum Testen und Ausnutzen

  • Valgrind -- Speicher-Debugging-Tool zur Erkennung nicht initialisierter Variablen.

  • AddressSanitizer -- Speicherfehler-Detektor für C/C++-Programme.

  • Burp Suite -- Web-Sicherheitstool zum Testen der Authentifizierung mit verschiedenen Eingaben.


CVE-Beispiele

  • CVE-2002-1374 -- Passwort nur gegen erstes Zeichen verglichen.

  • CVE-2000-0979 -- Einzelbyte-Passwortvergleich-Umgehung.

  • CVE-2001-0088 -- Nicht initialisiertes Array schneidet Passwort auf einzelnes Zeichen.


Referenzen

  1. MITRE Corporation. "CWE-305: Authentication Bypass by Primary Weakness." Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/305.html

  2. OWASP Foundation. "Authentication Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html

  3. CERT/CC. "Secure Coding Standards." https://wiki.sei.cmu.edu/confluence/display/seccode