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
| Auswirkung | Details |
|---|---|
| Vertraulichkeit | Umfang: Vertraulichkeit Unautorisierter Lesezugriff auf Anwendungsdaten durch geerbte Dateideskriptoren. |
| Integrität | Umfang: 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
- MITRE Corporation. "CWE-403: Exposure of File Descriptor to Unintended Control Sphere." https://cwe.mitre.org/data/definitions/403.html
- CERT C. "FIO42-C. Close files when they are no longer needed"
- OpenBSD. "Privilege Separation Guidelines"