Race Condition During Access to Alternate Channel

Description

Race Condition During Access to Alternate Channel occurs when a product establishes an alternate communication channel meant for authorized users, but fails to restrict access to other actors, creating a race condition vulnerability. This creates a race condition that allows an attacker to access the channel before the authorized user does. The vulnerability combines improper channel protection with timing-based exploitation, where attackers can intercept or hijack connections intended for legitimate users.

Risk

Race conditions in alternate channels have severe implications. Connection hijacking. Session theft. Port stealing. Man-in-the-middle attacks. Privilege escalation. Authentication bypass. Data interception. High likelihood in network services with predictable port allocation.

Solution

Implement proper access controls on all communication channels during architecture and design phase. Use cryptographic authentication for channel access. Avoid predictable port allocation. Implement connection binding with strong identifiers. Use time-limited tokens for channel access during implementation phase.

Common Consequences

ImpactDetails
Access ControlScope: Access Control

Gain Privileges or Assume Identity - Attackers can hijack channels intended for authorized users.
AuthorizationScope: Authorization

Bypass Protection Mechanism - Authentication can be bypassed by winning the race condition.

Example Code

Vulnerable Code

// Vulnerable: FTP data channel race condition (Pizza Thief vulnerability)

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

// VULNERABLE: Classic FTP PORT command 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 opens a port for data transfer
    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;  // Let OS assign port

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

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

    // VULNERABLE: Tell client the port number
    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);

    // VULNERABLE: Accept ANY connection on this port
    // Attacker can connect before legitimate client
    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);

    // VULNERABLE: No verification that connection is from legitimate client
    // Attacker's connection is accepted instead

    // Transfer sensitive data to attacker!
    send(client_data_fd, sensitive_file_data, data_len, 0);

    close(client_data_fd);
    close(data_fd);
}

// VULNERABLE: Named pipe race condition
void vulnerable_named_pipe() {
    const char* pipe_name = "\\\\.\\pipe\\myapp_auth";

    // VULNERABLE: Create pipe with weak security
    HANDLE pipe = CreateNamedPipe(
        pipe_name,
        PIPE_ACCESS_DUPLEX,
        PIPE_TYPE_MESSAGE | PIPE_WAIT,
        1,  // Only one instance
        1024, 1024,
        0,
        NULL  // VULNERABLE: No security descriptor!
    );

    // Wait for connection
    // VULNERABLE: Anyone can connect first
    ConnectNamedPipe(pipe, NULL);

    // Attacker connects before legitimate client
    // Receives authentication data meant for authorized process
}
# Vulnerable: Python callback server with race condition

import socket
import threading
import time

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

    def request_callback(self, user_id):
        """User requests a callback port for sensitive data."""
        # Create a temporary socket
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.bind(('0.0.0.0', 0))  # Random port
        sock.listen(1)

        port = sock.getsockname()[1]

        # VULNERABLE: Store callback with no secret
        self.pending_callbacks[user_id] = {
            'socket': sock,
            'port': port
        }

        # Tell user the port
        return port  # User gets port number

    def handle_callback(self, user_id):
        """Wait for callback connection."""
        if user_id not in self.pending_callbacks:
            return None

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

        # VULNERABLE: Accept ANY connection
        # No way to verify it's the legitimate user
        conn, addr = sock.accept()

        # VULNERABLE: Race condition - attacker may connect first
        # Send sensitive data to whoever connects
        conn.send(get_sensitive_data(user_id))

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

# Attack scenario:
# 1. Attacker monitors for new listening ports
# 2. When port opens, immediately connect
# 3. Receive data intended for legitimate user

# VULNERABLE: Database connection with predictable temp port
def vulnerable_db_callback(db_host, user_query):
    # Request async query result
    callback_port = random.randint(10000, 60000)  # VULNERABLE: Predictable range

    # Open port for callback
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('0.0.0.0', callback_port))
    sock.listen(1)

    # Tell database to send results here
    db.execute(f"NOTIFY {callback_port} WITH QUERY '{user_query}'")

    # VULNERABLE: Accept first connection
    conn, addr = sock.accept()

    # VULNERABLE: No verification that it's from db_host
    result = conn.recv(65536)

    return result
// Vulnerable: Java RMI callback race condition

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

public class VulnerableRMICallback {

    // VULNERABLE: Callback interface exposed
    public interface DataCallback extends Remote {
        void receiveData(byte[] data) throws RemoteException;
    }

    // Client implementation
    public class VulnerableClient {

        public void requestData(RemoteServer server) throws Exception {
            // Create callback object
            DataCallback callback = new DataCallbackImpl();

            // VULNERABLE: Export on random port
            UnicastRemoteObject.exportObject(callback, 0);

            // VULNERABLE: Register callback - server will call back
            server.registerCallback(callback);

            // Race condition: Attacker can discover the callback port
            // and send malicious data before real server responds
        }
    }

    // VULNERABLE: Server sends data without verifying callback authenticity
    public class VulnerableServer extends UnicastRemoteObject
            implements RemoteServer {

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

        public void registerCallback(DataCallback callback) {
            // VULNERABLE: No verification of callback source
            callbacks.add(callback);
        }

        public void notifyCallbacks(byte[] data) {
            for (DataCallback cb : callbacks) {
                try {
                    // VULNERABLE: Could be attacker's fake callback
                    cb.receiveData(data);
                } catch (RemoteException e) {
                    // Handle error
                }
            }
        }
    }
}

Fixed Code

// Fixed: Secure FTP data channel with proper verification

#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>

// FIXED: FTP data channel with client verification
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);

    // FIXED: Generate one-time token
    unsigned char token[16];
    RAND_bytes(token, sizeof(token));

    // Send port and token to client
    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);

    // FIXED: Set timeout for accept
    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 or error
    }

    // FIXED: Verify peer IP matches expected client
    if (peer_addr.sin_addr.s_addr != expected_client_addr) {
        close(client_data_fd);
        close(data_fd);
        send(control_fd, "425 Connection from wrong IP\r\n", 30, 0);
        return;
    }

    // FIXED: Require token verification over data channel
    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 verification failed\r\n", 31, 0);
        return;
    }

    // Now safe to transfer data
    send(client_data_fd, file_data, data_len, 0);

    close(client_data_fd);
    close(data_fd);
}

// FIXED: Named pipe with security descriptor
void secure_named_pipe() {
    const char* pipe_name = "\\\\.\\pipe\\myapp_auth";

    // FIXED: Create security descriptor
    SECURITY_ATTRIBUTES sa;
    SECURITY_DESCRIPTOR sd;
    InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);

    // FIXED: Only allow specific user/group
    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  // FIXED: Security descriptor
    );

    ConnectNamedPipe(pipe, NULL);

    // FIXED: Verify client identity
    ULONG client_pid;
    GetNamedPipeClientProcessId(pipe, &client_pid);

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

    // Process authenticated connection
}
# Fixed: Python callback server with proper authentication

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):
        """User requests a callback with authentication token."""
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        # FIXED: Bind to specific interface if possible
        sock.bind(('0.0.0.0', 0))
        sock.listen(1)

        # FIXED: Set timeout
        sock.settimeout(10.0)

        port = sock.getsockname()[1]

        # FIXED: Generate secure one-time token
        token = secrets.token_bytes(32)

        # FIXED: Store expected client info
        self.pending_callbacks[user_id] = {
            'socket': sock,
            'port': port,
            'token': token,
            'expected_ip': client_ip,
            'created': time.time()
        }

        # Return port and token (token should be sent securely)
        return port, token.hex()

    def handle_callback(self, user_id):
        """Wait for authenticated callback connection."""
        if user_id not in self.pending_callbacks:
            return None

        callback = self.pending_callbacks[user_id]

        # FIXED: Check for expired callback
        if time.time() - callback['created'] > 30:
            self._cleanup_callback(user_id)
            return None

        sock = callback['socket']

        try:
            conn, addr = sock.accept()

            # FIXED: Verify source IP
            if addr[0] != callback['expected_ip']:
                conn.close()
                raise SecurityError("Connection from unexpected IP")

            # FIXED: Require token authentication
            received_token = conn.recv(32)
            if not hmac.compare_digest(received_token, callback['token']):
                conn.close()
                raise SecurityError("Invalid token")

            # FIXED: Now safe to send data
            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]

# FIXED: Secure database callback
def secure_db_callback(db_host, user_query, db_shared_secret):
    # FIXED: Generate unpredictable token
    callback_token = secrets.token_hex(32)

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

    # FIXED: Bind to localhost only if possible
    sock.bind(('127.0.0.1', 0))
    sock.listen(1)
    sock.settimeout(30.0)

    callback_port = sock.getsockname()[1]

    # FIXED: Include authentication in callback request
    db.execute(f"NOTIFY {callback_port} WITH TOKEN {callback_token} QUERY '{user_query}'")

    try:
        conn, addr = sock.accept()

        # FIXED: Verify connection is from database host
        if addr[0] != db_host:
            conn.close()
            raise SecurityError("Connection not from database")

        # FIXED: First message must be the token
        received_token = conn.recv(64).decode()
        if received_token != callback_token:
            conn.close()
            raise SecurityError("Invalid callback token")

        result = conn.recv(65536)
        return result

    finally:
        sock.close()

CVE Examples

  • CVE-1999-0351: FTP "Pizza Thief" vulnerability allowing attackers to hijack data channel ports.
  • CVE-2003-0230: Windows named pipe hijacking during authentication.
  • CVE-2004-1002: Race condition in temporary file creation allowing symlink attacks.

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

References

  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"