Race Condition beim Zugriff auf Alternativkanal

Beschreibung

Race Condition beim Zugriff auf Alternativkanal tritt auf, wenn ein Produkt einen alternativen Kommunikationskanal für autorisierte Benutzer einrichtet, aber den Zugriff auf andere Akteure nicht einschränkt, wodurch eine Race-Condition-Schwachstelle entsteht. Dies erzeugt eine Race Condition, die einem Angreifer ermöglicht, auf den Kanal zuzugreifen, bevor der autorisierte Benutzer dies tut. Die Schwachstelle kombiniert unsachgemäßen Kanalschutz mit timing-basierter Ausnutzung, bei der Angreifer Verbindungen abfangen oder kapern können, die für legitime Benutzer bestimmt sind.

Risiko

Race Conditions in Alternativkanalen haben schwerwiegende Auswirkungen. Verbindungskapern. Sitzungsdiebstahl. Port-Stehlen. Man-in-the-Middle-Angriffe. Privilegieneskalation. Authentifizierungsumgehung. Datenabfangen. Hohe Wahrscheinlichkeit bei Netzwerkdiensten mit vorhersagbarer Port-Zuweisung.

Lösung

Implementieren Sie ordnungsgemäße Zugriffskontrollen auf allen Kommunikationskanalen während der Architektur- und Entwurfsphase. Verwenden Sie kryptographische Authentifizierung für Kanalzugriff. Vermeiden Sie vorhersagbare Port-Zuweisung. Implementieren Sie Verbindungsbindung mit starken Identifikatoren. Verwenden Sie zeitbegrenzte Tokens für Kanalzugriff während der Implementierungsphase.

Häufige Auswirkungen

AuswirkungDetails
ZugriffskontrolleUmfang: Zugriffskontrolle

Privilegien erlangen oder Identität annehmen - Angreifer können Kanäle kapern, die für autorisierte Benutzer bestimmt sind.
AutorisierungUmfang: Autorisierung

Schutzmechanismus umgehen - Authentifizierung kann durch Gewinnen der Race Condition umgangen werden.

Beispielcode

Anfälliger Code

// Anfällig: FTP Datenkanal Race Condition (Pizza-Dieb-Schwachstelle)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// ANFÄLLIG: Klassische FTP PORT-Befehl Race Condition
void vulnerable_ftp_data_channel(int control_fd, const char* client_ip) {
    struct sockaddr_in data_addr;
    int data_fd;
    int port;

    // Server öffnet einen Port für Datenübertragung
    data_fd = socket(AF_INET, SOCK_STREAM, 0);

    data_addr.sin_family = AF_INET;
    data_addr.sin_addr.s_addr = INADDR_ANY;
    data_addr.sin_port = 0;  // Betriebssystem weist Port zu

    bind(data_fd, (struct sockaddr*)&data_addr, sizeof(data_addr));
    listen(data_fd, 1);

    // Zugewiesenen Port abrufen
    socklen_t len = sizeof(data_addr);
    getsockname(data_fd, (struct sockaddr*)&data_addr, &len);
    port = ntohs(data_addr.sin_port);

    // ANFÄLLIG: Dem Client die Port-Nummer mitteilen
    char response[100];
    snprintf(response, sizeof(response),
             "227 Entering Passive Mode (127,0,0,1,%d,%d)\r\n",
             port / 256, port % 256);
    send(control_fd, response, strlen(response), 0);

    // ANFÄLLIG: JEDE Verbindung auf diesem Port akzeptieren
    // Angreifer kann sich vor dem legitimen Client verbinden
    struct sockaddr_in peer_addr;
    socklen_t peer_len = sizeof(peer_addr);
    int client_data_fd = accept(data_fd, (struct sockaddr*)&peer_addr, &peer_len);

    // ANFÄLLIG: Keine Verifikation, dass Verbindung vom legitimen Client kommt
    // Angreifers Verbindung wird stattdessen akzeptiert

    // Sensible Daten an Angreifer übertragen!
    send(client_data_fd, sensitive_file_data, data_len, 0);

    close(client_data_fd);
    close(data_fd);
}

// ANFÄLLIG: Named-Pipe Race Condition
void vulnerable_named_pipe() {
    const char* pipe_name = "\\\\.\\pipe\\myapp_auth";

    // ANFÄLLIG: Pipe mit schwacher Sicherheit erstellen
    HANDLE pipe = CreateNamedPipe(
        pipe_name,
        PIPE_ACCESS_DUPLEX,
        PIPE_TYPE_MESSAGE | PIPE_WAIT,
        1,  // Nur eine Instanz
        1024, 1024,
        0,
        NULL  // ANFÄLLIG: Kein Security-Deskriptor!
    );

    // Auf Verbindung warten
    // ANFÄLLIG: Jeder kann sich zuerst verbinden
    ConnectNamedPipe(pipe, NULL);

    // Angreifer verbindet sich vor dem legitimen Client
    // Empfängt Authentifizierungsdaten, die für autorisierten Prozess bestimmt sind
}
# Anfällig: Python Callback-Server mit Race Condition

import socket
import threading
import time

class VulnerableCallbackServer:
    def __init__(self):
        self.pending_callbacks = {}

    def request_callback(self, user_id):
        """Benutzer fordert einen Callback-Port für sensible Daten an."""
        # Temporären Socket erstellen
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.bind(('0.0.0.0', 0))  # Zufälliger Port
        sock.listen(1)

        port = sock.getsockname()[1]

        # ANFÄLLIG: Callback ohne Geheimnis speichern
        self.pending_callbacks[user_id] = {
            'socket': sock,
            'port': port
        }

        # Benutzer erhält Port-Nummer
        return port

    def handle_callback(self, user_id):
        """Auf Callback-Verbindung warten."""
        if user_id not in self.pending_callbacks:
            return None

        callback = self.pending_callbacks[user_id]
        sock = callback['socket']

        # ANFÄLLIG: JEDE Verbindung akzeptieren
        # Keine Möglichkeit zu verifizieren, dass es der legitime Benutzer ist
        conn, addr = sock.accept()

        # ANFÄLLIG: Race Condition - Angreifer kann sich zuerst verbinden
        # Sensible Daten an jeden senden, der sich verbindet
        conn.send(get_sensitive_data(user_id))

        conn.close()
        sock.close()
        del self.pending_callbacks[user_id]

# Angriffsszenario:
# 1. Angreifer überwacht neue lauschende Ports
# 2. Wenn Port öffnet, sofort verbinden
# 3. Daten empfangen, die für legitimen Benutzer bestimmt sind

# ANFÄLLIG: Datenbankverbindung mit vorhersagbarem Temp-Port
def vulnerable_db_callback(db_host, user_query):
    # Async-Abfrageergebnis anfordern
    callback_port = random.randint(10000, 60000)  # ANFÄLLIG: Vorhersagbarer Bereich

    # Port für Callback öffnen
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('0.0.0.0', callback_port))
    sock.listen(1)

    # Datenbank mitteilen, dass Ergebnisse hierher gesendet werden sollen
    db.execute(f"NOTIFY {callback_port} WITH QUERY '{user_query}'")

    # ANFÄLLIG: Erste Verbindung akzeptieren
    conn, addr = sock.accept()

    # ANFÄLLIG: Keine Verifikation, dass es von db_host kommt
    result = conn.recv(65536)

    return result
// Anfällig: Java RMI Callback Race Condition

import java.rmi.*;
import java.rmi.server.*;

public class VulnerableRMICallback {

    // ANFÄLLIG: Callback-Interface offengelegt
    public interface DataCallback extends Remote {
        void receiveData(byte[] data) throws RemoteException;
    }

    // Client-Implementierung
    public class VulnerableClient {

        public void requestData(RemoteServer server) throws Exception {
            // Callback-Objekt erstellen
            DataCallback callback = new DataCallbackImpl();

            // ANFÄLLIG: Auf zufälligem Port exportieren
            UnicastRemoteObject.exportObject(callback, 0);

            // ANFÄLLIG: Callback registrieren - Server wird zurückrufen
            server.registerCallback(callback);

            // Race Condition: Angreifer kann den Callback-Port entdecken
            // und bösartige Daten senden, bevor echter Server antwortet
        }
    }

    // ANFÄLLIG: Server sendet Daten ohne Callback-Authentizität zu verifizieren
    public class VulnerableServer extends UnicastRemoteObject
            implements RemoteServer {

        private List<DataCallback> callbacks = new ArrayList<>();

        public void registerCallback(DataCallback callback) {
            // ANFÄLLIG: Keine Verifikation der Callback-Quelle
            callbacks.add(callback);
        }

        public void notifyCallbacks(byte[] data) {
            for (DataCallback cb : callbacks) {
                try {
                    // ANFÄLLIG: Könnte gefälschter Callback des Angreifers sein
                    cb.receiveData(data);
                } catch (RemoteException e) {
                    // Fehler behandeln
                }
            }
        }
    }
}

Korrigierter Code

// Korrigiert: Sicherer FTP Datenkanal mit ordnungsgemäßer Verifikation

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <openssl/rand.h>

// KORRIGIERT: FTP Datenkanal mit Client-Verifikation
void secure_ftp_data_channel(int control_fd, const char* expected_client_ip,
                              uint32_t expected_client_addr) {
    struct sockaddr_in data_addr;
    int data_fd;
    int port;

    data_fd = socket(AF_INET, SOCK_STREAM, 0);

    data_addr.sin_family = AF_INET;
    data_addr.sin_addr.s_addr = INADDR_ANY;
    data_addr.sin_port = 0;

    bind(data_fd, (struct sockaddr*)&data_addr, sizeof(data_addr));
    listen(data_fd, 1);

    socklen_t len = sizeof(data_addr);
    getsockname(data_fd, (struct sockaddr*)&data_addr, &len);
    port = ntohs(data_addr.sin_port);

    // KORRIGIERT: Einmal-Token generieren
    unsigned char token[16];
    RAND_bytes(token, sizeof(token));

    // Port und Token an Client senden
    char response[200];
    snprintf(response, sizeof(response),
             "227 Entering Passive Mode (...) TOKEN=%s\r\n",
             bytes_to_hex(token, sizeof(token)));
    send(control_fd, response, strlen(response), 0);

    // KORRIGIERT: Timeout für accept setzen
    struct timeval tv;
    tv.tv_sec = 5;
    tv.tv_usec = 0;
    setsockopt(data_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

    struct sockaddr_in peer_addr;
    socklen_t peer_len = sizeof(peer_addr);
    int client_data_fd = accept(data_fd, (struct sockaddr*)&peer_addr, &peer_len);

    if (client_data_fd < 0) {
        close(data_fd);
        return;  // Timeout oder Fehler
    }

    // KORRIGIERT: Peer-IP muss erwartetem Client entsprechen
    if (peer_addr.sin_addr.s_addr != expected_client_addr) {
        close(client_data_fd);
        close(data_fd);
        send(control_fd, "425 Verbindung von falscher IP\r\n", 32, 0);
        return;
    }

    // KORRIGIERT: Token-Verifikation über Datenkanal erfordern
    unsigned char received_token[16];
    if (recv(client_data_fd, received_token, sizeof(received_token), 0) != sizeof(received_token) ||
        memcmp(token, received_token, sizeof(token)) != 0) {
        close(client_data_fd);
        close(data_fd);
        send(control_fd, "425 Token-Verifikation fehlgeschlagen\r\n", 39, 0);
        return;
    }

    // Jetzt sicher Daten übertragen
    send(client_data_fd, file_data, data_len, 0);

    close(client_data_fd);
    close(data_fd);
}

// KORRIGIERT: Named-Pipe mit Security-Deskriptor
void secure_named_pipe() {
    const char* pipe_name = "\\\\.\\pipe\\myapp_auth";

    // KORRIGIERT: Security-Deskriptor erstellen
    SECURITY_ATTRIBUTES sa;
    SECURITY_DESCRIPTOR sd;
    InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);

    // KORRIGIERT: Nur bestimmte Benutzer/Gruppe erlauben
    SetSecurityDescriptorDacl(&sd, TRUE, create_restricted_acl(), FALSE);

    sa.nLength = sizeof(sa);
    sa.lpSecurityDescriptor = &sd;
    sa.bInheritHandle = FALSE;

    HANDLE pipe = CreateNamedPipe(
        pipe_name,
        PIPE_ACCESS_DUPLEX,
        PIPE_TYPE_MESSAGE | PIPE_WAIT,
        1,
        1024, 1024,
        0,
        &sa  // KORRIGIERT: Security-Deskriptor
    );

    ConnectNamedPipe(pipe, NULL);

    // KORRIGIERT: Client-Identität verifizieren
    ULONG client_pid;
    GetNamedPipeClientProcessId(pipe, &client_pid);

    if (!is_authorized_process(client_pid)) {
        DisconnectNamedPipe(pipe);
        CloseHandle(pipe);
        return;
    }

    // Authentifizierte Verbindung verarbeiten
}
# Korrigiert: Python Callback-Server mit ordnungsgemäßer Authentifizierung

import socket
import secrets
import hmac
import hashlib
import time

class SecureCallbackServer:
    def __init__(self, secret_key):
        self.secret_key = secret_key
        self.pending_callbacks = {}

    def request_callback(self, user_id, client_ip):
        """Benutzer fordert Callback mit Authentifizierungstoken an."""
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        # KORRIGIERT: An spezifisches Interface binden wenn möglich
        sock.bind(('0.0.0.0', 0))
        sock.listen(1)

        # KORRIGIERT: Timeout setzen
        sock.settimeout(10.0)

        port = sock.getsockname()[1]

        # KORRIGIERT: Sicheres Einmal-Token generieren
        token = secrets.token_bytes(32)

        # KORRIGIERT: Erwartete Client-Infos speichern
        self.pending_callbacks[user_id] = {
            'socket': sock,
            'port': port,
            'token': token,
            'expected_ip': client_ip,
            'created': time.time()
        }

        # Port und Token zurückgeben (Token sollte sicher gesendet werden)
        return port, token.hex()

    def handle_callback(self, user_id):
        """Auf authentifizierte Callback-Verbindung warten."""
        if user_id not in self.pending_callbacks:
            return None

        callback = self.pending_callbacks[user_id]

        # KORRIGIERT: Auf abgelaufenen Callback prüfen
        if time.time() - callback['created'] > 30:
            self._cleanup_callback(user_id)
            return None

        sock = callback['socket']

        try:
            conn, addr = sock.accept()

            # KORRIGIERT: Quell-IP verifizieren
            if addr[0] != callback['expected_ip']:
                conn.close()
                raise SecurityError("Verbindung von unerwarteter IP")

            # KORRIGIERT: Token-Authentifizierung erfordern
            received_token = conn.recv(32)
            if not hmac.compare_digest(received_token, callback['token']):
                conn.close()
                raise SecurityError("Ungültiges Token")

            # KORRIGIERT: Jetzt sicher Daten senden
            conn.send(get_sensitive_data(user_id))

            return True

        except socket.timeout:
            return None
        finally:
            self._cleanup_callback(user_id)

    def _cleanup_callback(self, user_id):
        if user_id in self.pending_callbacks:
            self.pending_callbacks[user_id]['socket'].close()
            del self.pending_callbacks[user_id]

# KORRIGIERT: Sicherer Datenbank-Callback
def secure_db_callback(db_host, user_query, db_shared_secret):
    # KORRIGIERT: Unvorhersagbares Token generieren
    callback_token = secrets.token_hex(32)

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

    # KORRIGIERT: Nur an localhost binden wenn möglich
    sock.bind(('127.0.0.1', 0))
    sock.listen(1)
    sock.settimeout(30.0)

    callback_port = sock.getsockname()[1]

    # KORRIGIERT: Authentifizierung in Callback-Anfrage einschließen
    db.execute(f"NOTIFY {callback_port} WITH TOKEN {callback_token} QUERY '{user_query}'")

    try:
        conn, addr = sock.accept()

        # KORRIGIERT: Verbindung muss vom Datenbank-Host kommen
        if addr[0] != db_host:
            conn.close()
            raise SecurityError("Verbindung nicht von Datenbank")

        # KORRIGIERT: Erste Nachricht muss das Token sein
        received_token = conn.recv(64).decode()
        if received_token != callback_token:
            conn.close()
            raise SecurityError("Ungültiges Callback-Token")

        result = conn.recv(65536)
        return result

    finally:
        sock.close()

CVE-Beispiele

  • CVE-1999-0351: FTP "Pizza-Dieb"-Schwachstelle ermöglicht Angreifern das Kapern von Datenkanal-Ports.
  • CVE-2003-0230: Windows Named-Pipe-Kapern während Authentifizierung.
  • CVE-2004-1002: Race Condition bei temporärer Dateierstellung ermöglicht Symlink-Angriffe.

Verwandte CWEs

  • CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization (Eltern)
  • CWE-420: Unprotected Alternate Channel (Eltern)
  • CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition (verwandt)

Referenzen

  1. MITRE Corporation. "CWE-421: Race Condition During Access to Alternate Channel." https://cwe.mitre.org/data/definitions/421.html
  2. CERT. "FTP Bounce Attack and Port Stealing"
  3. Microsoft. "Named Pipe Security"