Offenlegung von Dateideskriptoren an unbeabsichtigte Kontrollsphäre ('Dateideskriptor-Leck')

Beschreibung

Offenlegung von Dateideskriptoren an unbeabsichtigte Kontrollsphäre tritt auf, wenn ein Prozess versäumt, sensible Dateideskriptoren zu schließen, bevor er einen Kindprozess erzeugt, was dem Kind ermöglicht, unautorisierte E/A-Operationen mit diesen Deskriptoren durchzuführen. Wenn ein neuer Prozess geforkt oder ausgeführt wird, erbt das Kind offene Dateideskriptoren von seinem Elternprozess. Dies schafft eine Schwachstelle, wenn das Kind weniger Privilegien als der Elternprozess hat, aber trotzdem auf den geerbten Deskriptor zugreifen kann, was potenziell Zugriffskontrollen auf die zugehörige Datei oder Ressource umgeht.

Risiko

Dateideskriptor-Lecks haben schwerwiegende Auswirkungen. Kindprozesse greifen auf privilegierte Dateien des Elternprozesses zu. Privilegieneskalation durch geerbte Deskriptoren. Unautorisierter Lese-/Schreibzugriff auf sensible Dateien. Umgehung von Dateiberechtigungen. Socket-Hijacking. Diebstahl von Datenbankverbindungen. Manipulation von Protokolldateien. Hohe Wahrscheinlichkeit wenn Privilegientrennung ohne ordnungsgemäße fd-Verwaltung implementiert wird.

Lösung

Schließen Sie alle sensiblen Dateideskriptoren bevor Kindprozesse während der Implementierungsphase erzeugt werden. Verwenden Sie das O_CLOEXEC-Flag beim Öffnen von Dateien. Setzen Sie FD_CLOEXEC auf bestehende Deskriptoren. Implementieren Sie das Close-all-fds-Muster vor exec. Auditieren Sie Deskriptor-Vererbung über Privilegiengrenzen hinweg während der Architektur- und Design-Phase.

Häufige Auswirkungen

AuswirkungDetails
VertraulichkeitUmfang: Vertraulichkeit

Unautorisierter Lesezugriff auf Anwendungsdaten durch geerbte Dateideskriptoren.
IntegritätUmfang: Integrität

Unautorisierte Modifikation von Anwendungsdaten durch geerbte Schreib-Deskriptoren.

Beispielcode

Anfälliger Code

// Anfällig: Dateideskriptor-Leck zu Kindprozess

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/socket.h>

// ANFÄLLIG: Privilegierter fd wird von unprivilegiertem Kind geerbt
void vulnerable_privilege_separation() {
    // Eltern öffnet sensible Datei als root
    int shadow_fd = open("/etc/shadow", O_RDWR);
    if (shadow_fd < 0) {
        return;
    }

    pid_t pid = fork();

    if (pid == 0) {
        // Kindprozess - Privilegien abgeben
        setuid(getuid());  // Auf normalen Benutzer absenken

        // ANFÄLLIG: Kind hat shadow_fd noch offen!
        // Kann /etc/shadow lesen/schreiben trotz fehlender Privilegien

        char buffer[1024];
        read(shadow_fd, buffer, sizeof(buffer));  // Funktioniert!

        // Angreifer kann Passwort-Hashes lesen
        printf("Shadow-Inhalte: %s\n", buffer);

        exit(0);
    }

    close(shadow_fd);
    waitpid(pid, NULL, 0);
}

// ANFÄLLIG: Socket-Deskriptor-Leck
void vulnerable_inetd_style() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    // ... bind, listen ...

    while (1) {
        int client_fd = accept(server_fd, NULL, NULL);

        pid_t pid = fork();
        if (pid == 0) {
            // Kind behandelt diesen Client

            // ANFÄLLIG: server_fd noch offen
            // Kind könnte andere Verbindungen akzeptieren
            // oder Server-Betrieb storen

            // Privilegien für Behandlung nicht vertrauenswürdiger Clients abgeben
            setuid(untrusted_uid);

            // Client-Anfrage verarbeiten
            handle_request(client_fd);

            // ANFÄLLIG: Kann server_fd noch benutzen
            // um Verbindungen als ursprünglicher Benutzer zu akzeptieren

            exit(0);
        }

        close(client_fd);  // Eltern schließt client fd
        // Aber Kind hat beide fds!
    }
}

// ANFÄLLIG: Protokolldatei-Deskriptor-Leck
void vulnerable_logging() {
    // Protokolldatei mit erhöhten Privilegien öffnen
    int log_fd = open("/var/log/secure.log", O_WRONLY | O_APPEND);

    // Worker-Prozess forken
    pid_t pid = fork();

    if (pid == 0) {
        // Worker mit reduzierten Privilegien
        setgid(worker_gid);
        setuid(worker_uid);

        // ANFÄLLIG: Kann noch in sicheres Protokoll schreiben
        // Könnte falsche Protokolleinträge einfügen
        const char* fake = "root logged in from 127.0.0.1\n";
        write(log_fd, fake, strlen(fake));

        do_work();
        exit(0);
    }

    close(log_fd);
    waitpid(pid, NULL, 0);
}
# Anfällig: Python Dateideskriptor-Vererbung

import os
import subprocess

# ANFÄLLIG: Dateideskriptor wird von Subprocess geerbt
def vulnerable_subprocess():
    # Sensible Konfiguration öffnen
    config_fd = os.open('/etc/app/secrets.conf', os.O_RDONLY)

    # ANFÄLLIG: Subprocess erbt den fd
    proc = subprocess.Popen(
        ['untrusted_program'],
        # close_fds ist in älterem Python standardmäßig False!
    )

    # untrusted_program kann auf Konfigurationsdatei zugreifen
    # über /proc/self/fd/{config_fd}

    proc.wait()
    os.close(config_fd)

# ANFÄLLIG: Multiprocessing mit gemeinsamen Deskriptoren
from multiprocessing import Process

class VulnerableWorker:
    def __init__(self):
        # Privilegierte Ressource öffnen
        self.secret_file = open('/root/.ssh/id_rsa', 'r')

    def start_worker(self):
        # ANFÄLLIG: Worker-Prozess erbt secret_file
        p = Process(target=self.worker_main)
        p.start()
        return p

    def worker_main(self):
        # Worker kann SSH-Schlüssel lesen!
        key_data = self.secret_file.read()
        # ... potenzielle Exfiltration ...

Korrigierter Code

// Korrigiert: Ordnungsgemäße Dateideskriptor-Verwaltung

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/resource.h>
#include <dirent.h>

// KORRIGIERT: Sensiblen fd vor Fork schließen
void safe_privilege_separation() {
    int shadow_fd = open("/etc/shadow", O_RDWR);
    if (shadow_fd < 0) {
        return;
    }

    // Lesen was wir brauchen solange privilegiert
    char shadow_data[4096];
    ssize_t len = read(shadow_fd, shadow_data, sizeof(shadow_data) - 1);

    // KORRIGIERT: Vor Fork schließen
    close(shadow_fd);
    shadow_fd = -1;

    pid_t pid = fork();

    if (pid == 0) {
        // Kind - kein Zugriff auf shadow_fd
        setuid(getuid());

        // Daten verarbeiten die bereits gelesen wurden
        // Kann nicht direkt auf /etc/shadow zugreifen

        exit(0);
    }

    // Sensible Daten löschen
    memset(shadow_data, 0, sizeof(shadow_data));
    waitpid(pid, NULL, 0);
}

// KORRIGIERT: O_CLOEXEC-Flag verwenden
void safe_with_cloexec() {
    // KORRIGIERT: Datei wird bei exec geschlossen
    int secret_fd = open("/etc/shadow", O_RDONLY | O_CLOEXEC);

    pid_t pid = fork();

    if (pid == 0) {
        // Wenn wir exec aufrufen, wird secret_fd automatisch geschlossen
        // Für fork-only müssen wir noch manuell schließen

        // KORRIGIERT: Im Kind schließen wenn exec nicht verwendet wird
        close(secret_fd);

        // Jetzt sicher Privilegien abzugeben
        setuid(getuid());

        // ... Arbeit erledigen ...
        exit(0);
    }

    close(secret_fd);
    waitpid(pid, NULL, 0);
}

// KORRIGIERT: Alle Dateideskriptoren schließen
void close_all_fds(void) {
    // Methode 1: /proc/self/fd verwenden
    DIR* dir = opendir("/proc/self/fd");
    if (dir) {
        int dir_fd = dirfd(dir);
        struct dirent* entry;

        while ((entry = readdir(dir)) != NULL) {
            int fd = atoi(entry->d_name);
            if (fd > STDERR_FILENO && fd != dir_fd) {
                close(fd);
            }
        }
        closedir(dir);
        return;
    }

    // Methode 2: Durch mögliche fds iterieren
    struct rlimit rl;
    getrlimit(RLIMIT_NOFILE, &rl);

    for (int fd = STDERR_FILENO + 1; fd < (int)rl.rlim_cur; fd++) {
        close(fd);
    }
}

// KORRIGIERT: Sicherer Server mit fd-Verwaltung
void safe_inetd_style() {
    // KORRIGIERT: Server-Socket mit close-on-exec
    int server_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
    // ... bind, listen ...

    while (1) {
        // KORRIGIERT: Accept mit close-on-exec
        int client_fd = accept4(server_fd, NULL, NULL, SOCK_CLOEXEC);

        pid_t pid = fork();
        if (pid == 0) {
            // KORRIGIERT: Server-fd im Kind schließen
            close(server_fd);

            // Close-on-exec für client_fd löschen falls nötig
            fcntl(client_fd, F_SETFD, 0);

            // Jetzt sicher Privilegien abzugeben
            setuid(untrusted_uid);

            handle_request(client_fd);
            close(client_fd);
            exit(0);
        }

        close(client_fd);
    }
}

// KORRIGIERT: Close-on-exec auf bestehendem fd setzen
void set_cloexec(int fd) {
    int flags = fcntl(fd, F_GETFD);
    if (flags >= 0) {
        fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
    }
}
# Korrigiert: Python mit ordnungsgemäßer fd-Isolierung

import os
import subprocess
import fcntl

# KORRIGIERT: fds vor Subprocess schließen
def safe_subprocess():
    config_fd = os.open('/etc/app/secrets.conf', os.O_RDONLY)

    # Lesen was wir brauchen
    config_data = os.read(config_fd, 4096)

    # KORRIGIERT: Vor Subprocess schließen
    os.close(config_fd)

    # KORRIGIERT: close_fds=True (Standard in Python 3.2+)
    proc = subprocess.Popen(
        ['untrusted_program'],
        close_fds=True  # Explizit gesetzt für Klarheit
    )

    proc.wait()

# KORRIGIERT: O_CLOEXEC-Äquivalent verwenden
def safe_open_cloexec(path, flags):
    fd = os.open(path, flags)
    # KORRIGIERT: Close-on-exec-Flag setzen
    fcntl.fcntl(fd, fcntl.F_SETFD, fcntl.FD_CLOEXEC)
    return fd

# KORRIGIERT: Multiprocessing mit ordnungsgemäßer Isolierung
from multiprocessing import Process
import multiprocessing

class SafeWorker:
    def __init__(self):
        # Keine sensiblen Dateien im Elternprozess öffnen
        self.secret_path = '/root/.ssh/id_rsa'

    def start_worker(self, task_data):
        # KORRIGIERT: Nur notwendige Daten übergeben, keine Datei-Handles
        p = Process(target=self.worker_main, args=(task_data,))
        p.start()
        return p

    def worker_main(self, task_data):
        # Worker öffnet eigene Dateien mit entsprechenden Berechtigungen
        # Keine geerbten sensiblen Deskriptoren
        pass

# KORRIGIERT: Kontextmanager für automatischen Cleanup
class SecureFD:
    def __init__(self, path, flags):
        self.fd = os.open(path, flags | os.O_CLOEXEC)

    def __enter__(self):
        return self.fd

    def __exit__(self, *args):
        os.close(self.fd)

# Verwendung
with SecureFD('/etc/shadow', os.O_RDONLY) as fd:
    data = os.read(fd, 4096)
# fd automatisch geschlossen, sicher zu fork/exec

CVE-Beispiele

  • CVE-2003-0740: Sendmail Dateideskriptor-Leck ermöglichte Privilegieneskalation.
  • CVE-2003-0937: OpenSSH Dateideskriptor-Leck bei Privilegientrennung.
  • CVE-2006-5397: Screen Dateideskriptor-Leck erlaubte unautorisierten Zugriff.

Verwandte CWEs

  • CWE-402: Transmission of Private Resources into a New Sphere (Eltern)
  • CWE-399: Resource Management Errors (Kategorie)

Referenzen

  1. MITRE Corporation. "CWE-403: Exposure of File Descriptor to Unintended Control Sphere." https://cwe.mitre.org/data/definitions/403.html
  2. CERT C. "FIO42-C. Close files when they are no longer needed"
  3. OpenBSD. "Privilege Separation Guidelines"