Unsachgemäße Validierung von Zertifikaten mit Host-Diskrepanz

Beschreibung

Unsachgemäße Validierung von Zertifikaten mit Host-Diskrepanz ist eine Schwachstelle, die auftritt, wenn ein Produkt Zertifikate akzeptiert, ohne zu verifizieren, dass sie zum beabsichtigten Host gehören. Ein Zertifikat kann vollständig gültig sein, ordnungsgemäß von einer vertrauenswürdigen CA signiert und nicht abgelaufen, aber für eine andere Domain als diejenige ausgestellt sein, mit der verbunden wird. Ohne Hostname-Validierung können Angreifer legitime Zertifikate für ihre eigenen Domains erhalten und sie während Man-in-the-Middle-Angriffen oder Umleitungsszenarien verwenden. Das Produkt muss die host-spezifischen Daten des Zertifikats validieren, insbesondere den Common Name (CN) im Subject-Feld oder, vorzugsweise, die Subject Alternative Name (SAN)-Erweiterung.

Risiko

Fehlende Hostname-Validierung macht den gesamten Zertifikatsverifikationsprozess gegen gezielte Angriffe nahezu nutzlos. Angreifer können leicht gültige Zertifikate von Zertifizierungsstellen für Domains erhalten, die sie kontrollieren. Wenn die Anwendung eines Opfers sich mit einem vermeintlich vertrauenswürdigen Server verbindet, präsentiert ein Man-in-the-Middle-Angreifer sein legitimes Zertifikat. Ohne Hostname-Prüfung akzeptiert die Anwendung dieses Zertifikat, weil es technisch gültig ist, was dem Angreifer ermöglicht, den gesamten Verkehr abzufangen. CVE-2012-5810 demonstrierte echte finanzielle Verluste durch eine Mobile-Banking-Anwendung, die Hostnamen nicht verifizierte. Implementierungsfehler wie unsachgemäße Null-Byte-Behandlung können ebenfalls die Hostname-Validierung aushebeln, selbst wenn sie versucht wird.

Lösung

Validieren Sie immer, dass der Hostname des Zertifikats mit dem erwarteten Server-Hostname übereinstimmt. Prüfen Sie sowohl den Common Name (CN) im Subject-Feld als auch die Subject Alternative Name (SAN)-Erweiterung - moderne Zertifikate verwenden SAN, und RFC 6125 empfiehlt, SAN bevorzugt zu prüfen. Verwenden Sie TLS-Bibliotheksfunktionen, die für Hostname-Validierung konzipiert sind, anstatt benutzerdefinierte Prüfungen zu implementieren. Behandeln Sie Randfälle einschließlich Wildcards, internationalisierten Domain-Namen (IDN) und Null-Bytes in Zertifikatsnamen. Bei Verwendung von Certificate Pinning validieren Sie alle Zertifikatseigenschaften einschließlich Hostname bevor Pinning stattfindet. Kommunizieren Sie Verifikationsergebnisse klar an Benutzer, damit sie verstehen, wenn Verbindungen möglicherweise unsicher sind. Testen Sie mit Zertifikaten, die für verschiedene Hostnamen gültig sind, um sicherzustellen, dass Hostname-Validierung funktioniert.

Häufige Auswirkungen

AuswirkungDetails
Zugriffskontrolle, AuthentifizierungBereich: Zugriffskontrolle, Authentifizierung

Daten vom validierten System können tatsächlich vom Host eines Angreifers stammen, der ein gültiges Zertifikat für eine andere Domain präsentiert. Dies ermöglicht Identitätsfälschung und Man-in-the-Middle-Angriffe, bei denen Angreifer alle Kommunikationen abfangen, lesen und modifizieren können.

Beispielcode

Anfälliger Code (C/OpenSSL)

Die folgenden Beispiele demonstrieren unsachgemäße Hostname-Validierung:

// Anfällig: Fehlende Hostname-Verifikation
#include <openssl/ssl.h>
#include <openssl/x509.h>

int vulnerable_verify_cert(SSL *ssl, const char *hostname) {
    X509 *cert = SSL_get_peer_certificate(ssl);

    if (cert && (SSL_get_verify_result(ssl) == X509_V_OK)) {
        // Anfällig: Zertifikatskette validiert, aber Hostname NICHT geprüft!
        // Angreifer kann JEDES gültige Zertifikat verwenden
        return 1;  // "Vertrauenswürdig" - aber für falschen Host!
    }

    return 0;
}

void vulnerable_connect(const char *hostname, int port) {
    SSL *ssl = create_ssl_connection(hostname, port);

    // Prüft nur ob Zertifikat gültig ist, nicht ob es für diesen Host ist
    if (SSL_get_verify_result(ssl) == X509_V_OK) {
        // Fährt ohne Hostname-Prüfung fort
        send_sensitive_data(ssl);
    }
}
# Anfällig: Python mit deaktivierter Hostname-Prüfung
import ssl
import socket

def vulnerable_connect(hostname, port):
    context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    context.verify_mode = ssl.CERT_REQUIRED
    context.load_default_certs()

    # Anfällig: Hostname-Prüfung explizit deaktiviert
    context.check_hostname = False

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    ssl_sock = context.wrap_socket(sock)  # Fehlender server_hostname-Parameter!

    ssl_sock.connect((hostname, port))
    # Zertifikatskette validiert, aber Hostname NICHT verifiziert
    return ssl_sock

def vulnerable_requests():
    import requests
    from requests.packages.urllib3.exceptions import InsecureRequestWarning

    # Anfällig: Benutzerdefinierter Adapter ignoriert Hostname
    # Manche Implementierungen tun dies
    requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

    # Oder benutzerdefinierter SSL-Kontext ohne Hostname-Prüfung
    return requests.get('https://example.com', verify=True)
// Anfällig: Java mit permissivem Hostname-Verifier
import javax.net.ssl.*;
import java.security.cert.X509Certificate;

public class VulnerableHostnameCheck {

    public static void vulnerable_setup() throws Exception {
        // Anfällig: HostnameVerifier der alle Hostnamen akzeptiert
        HttpsURLConnection.setDefaultHostnameVerifier(
            (hostname, session) -> true  // Gibt immer true zurück!
        );

        // Jetzt wird jedes Zertifikat für jeden Hostname akzeptiert
        URL url = new URL("https://secure.example.com");
        HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
        // Angreifer kann mit Zertifikat für "evil.attacker.com" abfangen
    }

    public void vulnerable_connect(String targetHost) throws Exception {
        SSLContext ctx = SSLContext.getInstance("TLS");
        ctx.init(null, getDefaultTrustManagers(), null);

        SSLSocketFactory factory = ctx.getSocketFactory();
        SSLSocket socket = (SSLSocket) factory.createSocket(targetHost, 443);

        socket.startHandshake();

        // Anfällig: Holt Zertifikat aber prüft nicht Hostname
        SSLSession session = socket.getSession();
        X509Certificate[] certs = (X509Certificate[]) session.getPeerCertificates();

        // Zertifikat ist gültig aber könnte für anderen Host sein!
        sendSensitiveData(socket);
    }
}

Korrigierter Code (C/OpenSSL)

// Korrigiert: Ordnungsgemäße Hostname-Verifikation
#include <openssl/ssl.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>

int secure_verify_cert(SSL *ssl, const char *expected_hostname) {
    X509 *cert = SSL_get_peer_certificate(ssl);

    if (cert == NULL) {
        fprintf(stderr, "Kein Zertifikat präsentiert\n");
        return 0;
    }

    // Zertifikatskettenvalidierung prüfen
    long verify_result = SSL_get_verify_result(ssl);
    if (verify_result != X509_V_OK) {
        fprintf(stderr, "Zertifikatsverifikation fehlgeschlagen: %s\n",
                X509_verify_cert_error_string(verify_result));
        X509_free(cert);
        return 0;
    }

    // Korrigiert: Hostname stimmt mit Zertifikat überein
    // X509_check_host behandelt CN, SAN, Wildcards und Null-Bytes
    int hostname_match = X509_check_host(cert, expected_hostname,
                                         strlen(expected_hostname),
                                         X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS,
                                         NULL);

    X509_free(cert);

    if (hostname_match != 1) {
        fprintf(stderr, "Hostname-Verifikation fehlgeschlagen für: %s\n",
                expected_hostname);
        return 0;
    }

    return 1;  // Zertifikat vollständig validiert einschließlich Hostname
}

// Alternative: Verwendung von SSL_set1_host für automatische Verifikation
SSL* secure_connect_with_auto_verify(SSL_CTX *ctx, const char *hostname, int port) {
    SSL *ssl = SSL_new(ctx);

    // Erwarteten Hostname für automatische Verifikation setzen
    if (!SSL_set1_host(ssl, hostname)) {
        fprintf(stderr, "Konnte Hostname für Verifikation nicht setzen\n");
        SSL_free(ssl);
        return NULL;
    }

    // Verbinden und Handshake
    BIO *bio = BIO_new_ssl_connect(ctx);
    BIO_set_conn_hostname(bio, hostname);
    BIO_set_conn_port(bio, "443");

    if (BIO_do_connect(bio) <= 0) {
        fprintf(stderr, "Verbindung fehlgeschlagen\n");
        return NULL;
    }

    // Hostname-Verifikation erfolgt automatisch während Handshake
    return ssl;
}
# Korrigiert: Python mit ordnungsgemäßer Hostname-Verifikation
import ssl
import socket
import certifi

def secure_connect(hostname, port):
    context = ssl.create_default_context(cafile=certifi.where())

    # Korrigiert: Hostname-Verifikation ist aktiviert (Standard in Python 3.7+)
    context.check_hostname = True
    context.verify_mode = ssl.CERT_REQUIRED

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # Korrigiert: server_hostname für SNI und Hostname-Verifikation bereitstellen
    ssl_sock = context.wrap_socket(sock, server_hostname=hostname)

    try:
        ssl_sock.connect((hostname, port))
    except ssl.CertificateError as e:
        # Dies schließt Hostname-Diskrepanzen ein
        print(f"Zertifikatsfehler (möglicherweise Hostname-Diskrepanz): {e}")
        raise

    return ssl_sock

def secure_requests():
    import requests

    # Korrigiert: verify=True aktiviert sowohl Ketten- als auch Hostname-Validierung (Standard)
    response = requests.get('https://example.com', verify=True)

    # Oder mit explizitem CA-Bundle
    response = requests.get('https://example.com', verify=certifi.where())

    return response
// Korrigiert: Java mit ordnungsgemäßer Hostname-Verifikation
import javax.net.ssl.*;
import java.security.cert.X509Certificate;

public class SecureHostnameCheck {

    public static void secure_setup() throws Exception {
        // Korrigiert: Standard-HostnameVerifier verwenden (validiert Hostname)
        // Nicht mit permissivem Verifier überschreiben
        // HttpsURLConnection verwendet standardmäßig HTTPS-Hostname-Verifier
    }

    public void secure_connect(String targetHost, int port) throws Exception {
        SSLContext ctx = SSLContext.getInstance("TLS");
        ctx.init(null, getDefaultTrustManagers(), null);

        SSLSocketFactory factory = ctx.getSocketFactory();
        SSLSocket socket = (SSLSocket) factory.createSocket(targetHost, port);

        // Korrigiert: Erwarteten Hostname für Verifikation setzen
        SSLParameters params = socket.getSSLParameters();
        params.setEndpointIdentificationAlgorithm("HTTPS");  // Aktiviert Hostname-Prüfung
        socket.setSSLParameters(params);

        socket.startHandshake();

        // Korrigiert: Hostname wird automatisch während Handshake verifiziert
        // Bei Diskrepanz wird SSLException geworfen

        sendSensitiveData(socket);
    }

    // Alternative mit HttpsURLConnection
    public void secure_https_connect(String url) throws Exception {
        URL target = new URL(url);
        HttpsURLConnection conn = (HttpsURLConnection) target.openConnection();

        // Standard-Hostname-Verifier validiert gegen Zertifikat
        // Nur überschreiben wenn STRENGERE Prüfung nötig, nie permissiver

        conn.connect();
        // Hostname automatisch verifiziert
    }
}

Die Korrektur stellt sicher, dass Hostname-Verifikation aktiviert ist und der Hostname des Zertifikats (CN oder SAN) mit dem erwarteten Server übereinstimmt.


Ausgenutzt in der Praxis

Mobile-Banking-Hostname-Umgehung (Banking-Apps, 2012)

CVE-2012-5810 dokumentierte eine Mobile-Banking-Anwendung, die Hostnamen in TLS-Zertifikaten nicht verifizierte, was echte finanzielle Verluste verursachte, wenn Benutzer sich über kompromittierte Netzwerke verbanden und Angreifer Banking-Transaktionen abfingen.

Null-Byte in Zertifikatsname (Browser, 2009)

CVE-2009-2408 dokumentierte Browser, die Null-Bytes in Common-Name-Feldern nicht handhaben könnten, was Zertifikaten wie "www.bank.com\0.attacker.com" ermöglichte, die Hostname-Validierung für "www.bank.com" zu bestehen.

Python-Bibliothek Hostname-Regex (Python-Bibliotheken, 2012)

CVE-2012-3446 dokumentierte eine Python-Bibliothek, die fehlerhafte Regex für Hostname-Matching verwendete, was Umgehungen durch manipulierte Zertifikatsnamen ermöglichte.


Tools zum Testen/Ausnutzen

  • testssl.sh — Kommandozeilentool zum Testen von SSL/TLS einschließlich Hostname-Verifikation.

  • mitmproxy — HTTPS-Proxy zum Testen mit Zertifikaten für verschiedene Hostnamen.

  • badssl.com — Testseite mit verschiedenen Zertifikatsfehlerszenarien einschließlich Hostname-Diskrepanzen.


CVE-Beispiele

  • CVE-2009-2408 — Browser behandelt Null-Bytes im Common Name nicht, ermöglicht HTTPS-Spoofing.

  • CVE-2012-5810 — Mobile-Banking-App verifiziert Hostname nicht, verursacht finanzielle Verluste.

  • CVE-2012-3446 — Python-Bibliothek verwendet fehlerhafte Regex für Hostname-Matching.

  • CVE-2012-0867 — Datenbankprogramm kürzt Common Name während Verifikation.

  • CVE-2003-0355 — Webbrowser validiert Common Name nicht.


Referenzen

  1. MITRE Corporation. "CWE-297: Improper Validation of Certificate with Host Mismatch." Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/297.html

  2. RFC 6125. "Representation and Verification of Domain-Based Application Service Identity within Internet Public Key Infrastructure Using X.509 (PKIX) Certificates." https://tools.ietf.org/html/rfc6125

  3. OWASP Foundation. "Transport Layer Protection Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Protection_Cheat_Sheet.html