Unsichere Operation auf Windows Junction / Mount Point

Beschreibung

Unsichere Operation auf Windows Junction / Mount Point tritt auf, wenn ein Produkt eine Datei oder ein Verzeichnis öffnet, ohne ordnungsgemäß zu verhindern, dass es mit einem Junction oder Mount Point assoziiert ist, der auf Ressourcen außerhalb der beabsichtigten Kontrollsphäre zeigt. Diese Schwachstelle ermöglicht es Angreifern, Windows NTFS Reparse Points (Junctions und Mount Points) auszunutzen. Ein privilegiertes Programm, das auf eine Datei zugreift, kann dazu verleitet werden, unautorisierte Dateien zu lesen, zu schreiben oder zu löschen, wenn ein Angreifer das Ziel durch einen Hard Link auf eine sensible Datei ersetzt. Dies ermöglicht Rechteeskalation und Datenmanipulationsangriffe.

Risiko

Junction- und Mount-Point-Angriffe haben schwerwiegende Auswirkungen. Rechteeskalation durch Dateimanipulation. Beliebiges Dateilesen durch privilegierte Prozesse. Beliebiges Dateischreiben oder -ändern. Löschung kritischer Systemdateien. Permanenter Denial of Service durch Dateilöschung. Ausnutzung von Installern zur Codeausführung. Umgehung von Zugangskontrollen. Hohe Wahrscheinlichkeit, wenn privilegierte Programme auf benutzerbeschreibbare Verzeichnisse zugreifen.

Lösung

Bei der Entwicklung von Software mit anderen Rechten als der Ausführende prüfen, ob Dateien, mit denen interagiert wird, keine unsachgemäßen Hard Links oder Mount Points sind, während der Architektur- und Designphase. Windows-Befehle wie dir /al /s /b oder PowerShells LinkType-Filter verwenden, um Junctions zu erkennen. Dateiauthentifizierung über Signierung implementieren. Sicherstellen, dass Prüfungen atomar mit Dateiaktionen sind, um TOCTOU-Race-Conditions während der Implementierungsphase zu vermeiden.

Häufige Auswirkungen

AuswirkungDetails
VertraulichkeitBereich: Vertraulichkeit

Angreifer können beliebige Dateien lesen, indem sie benutzerkontrollierte Ordner durch Mount Points und Hard Links ersetzen.
IntegritätBereich: Integrität

Beliebige Dateimodifikation möglich, insbesondere Installer-Rollback-Dateien, die bei Neuinstallation ausgeführt werden.
VerfügbarkeitBereich: Verfügbarkeit

Löschung kritischer Dateien bei Ausführung als SYSTEM/Admin kann permanenten Denial of Service verursachen.

Beispielcode

Verwundbarer Code

// VERWUNDBAR: C-Code ohne Junction/Mount-Point-Schutz

#include <windows.h>
#include <stdio.h>

// VERWUNDBAR: Keine Junction-Point-Erkennung
BOOL vulnerable_write_log(const char* log_dir, const char* message) {
    char log_path[MAX_PATH];
    snprintf(log_path, MAX_PATH, "%s\\app.log", log_dir);

    // VERWUNDBAR: Öffnet Datei ohne Prüfung auf Reparse Points
    HANDLE hFile = CreateFileA(
        log_path,
        GENERIC_WRITE,
        0,
        NULL,
        OPEN_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL
    );

    if (hFile == INVALID_HANDLE_VALUE) {
        return FALSE;
    }

    // Angriffsszenario:
    // 1. Angreifer erstellt Junction: log_dir -> C:\Windows\System32
    // 2. Privilegierter Prozess schreibt in log_dir\app.log
    // 3. Schreibt tatsächlich in C:\Windows\System32\app.log
    // 4. Beliebiges Dateischreiben im Systemverzeichnis

    DWORD written;
    WriteFile(hFile, message, strlen(message), &written, NULL);
    CloseHandle(hFile);
    return TRUE;
}

// VERWUNDBAR: Installer-Bereinigung ohne Junction-Schutz
void vulnerable_cleanup_temp(const char* temp_dir) {
    char search_path[MAX_PATH];
    snprintf(search_path, MAX_PATH, "%s\\*", temp_dir);

    WIN32_FIND_DATAA find_data;
    HANDLE hFind = FindFirstFileA(search_path, &find_data);

    if (hFind != INVALID_HANDLE_VALUE) {
        do {
            if (strcmp(find_data.cFileName, ".") == 0 ||
                strcmp(find_data.cFileName, "..") == 0) {
                continue;
            }

            char file_path[MAX_PATH];
            snprintf(file_path, MAX_PATH, "%s\\%s", temp_dir, find_data.cFileName);

            // VERWUNDBAR: Löscht ohne Prüfung auf Reparse Points
            if (find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
                RemoveDirectoryA(file_path);  // Kann echtes Verzeichnis löschen
            } else {
                DeleteFileA(file_path);  // Kann über Hard Link löschen
            }

            // Angriffsszenario:
            // 1. Junction erstellen: temp_dir\subdir -> C:\Windows\System32
            // 2. Oder Hard Link erstellen: temp_dir\file.txt -> C:\Windows\System32\config.sys
            // 3. Privilegierte Bereinigung löscht Systemdateien

        } while (FindNextFileA(hFind, &find_data));
        FindClose(hFind);
    }
}

// VERWUNDBAR: Service-Konfigurationsaktualisierung
BOOL vulnerable_update_config(const char* user_config_dir) {
    char config_path[MAX_PATH];
    snprintf(config_path, MAX_PATH, "%s\\service.conf", user_config_dir);

    // VERWUNDBAR: Kopiert Benutzerdatei an Systemstandort ohne Validierung
    char system_config[] = "C:\\ProgramData\\MyService\\service.conf";

    // Angriff: user_config_dir\service.conf ist Hard Link auf bösartige Datei
    // oder user_config_dir ist Junction auf angreiferkontrollierten Speicherort
    return CopyFileA(config_path, system_config, FALSE);
}
// VERWUNDBAR: C#-Code ohne Symlink-Schutz

using System;
using System.IO;

public class VulnerableFileOperations
{
    // VERWUNDBAR: Keine Prüfung auf Reparse Points
    public void VulnerableWriteData(string directory, string data)
    {
        string filePath = Path.Combine(directory, "data.txt");

        // VERWUNDBAR: Schreibt ohne Validierung des Ziels
        File.WriteAllText(filePath, data);

        // Angriff: directory ist Junction, die anderswohin zeigt
        // Privilegierter Prozess schreibt an unbeabsichtigten Speicherort
    }

    // VERWUNDBAR: Verzeichnislöschung ohne Junction-Prüfung
    public void VulnerableCleanupDirectory(string path)
    {
        // VERWUNDBAR: Löscht rekursiv ohne Prüfung auf Junctions
        if (Directory.Exists(path))
        {
            Directory.Delete(path, true);  // recursive = true

            // Angriff:
            // 1. Angreifer erstellt: path\subdir -> C:\Windows\System32
            // 2. Delete(path, true) folgt der Junction
            // 3. System32-Inhalte gelöscht
        }
    }

    // VERWUNDBAR: Ausnutzung des Installer-Rollbacks
    public void VulnerableInstallFile(string sourcePath, string destPath)
    {
        // VERWUNDBAR: Keine Verifikation des Quellstandorts
        File.Copy(sourcePath, destPath, true);

        // Angriff: sourcePath zeigt auf bösartige Datei über Hard Link
        // Installer kopiert bösartigen Inhalt an geschützten Speicherort
    }

    // VERWUNDBAR: Service schreibt in benutzerkontrollierbaren Pfad
    public void VulnerableServiceLog(string userLogDir, string message)
    {
        // Service läuft als SYSTEM
        string logPath = Path.Combine(userLogDir, "service.log");

        // VERWUNDBAR: Benutzer kann über Junction umleiten
        using (StreamWriter sw = File.AppendText(logPath))
        {
            sw.WriteLine(message);
        }
    }
}

Sichere Lösung

// SICHER: C-Code mit Junction/Mount-Point-Schutz

#include <windows.h>
#include <stdio.h>

// SICHER: Prüfen ob Pfad einen Reparse Point enthält
BOOL is_reparse_point(const char* path) {
    DWORD attrs = GetFileAttributesA(path);
    if (attrs == INVALID_FILE_ATTRIBUTES) {
        return FALSE;
    }
    return (attrs & FILE_ATTRIBUTE_REPARSE_POINT) != 0;
}

// SICHER: Gesamten Pfad auf Junctions prüfen
BOOL path_contains_junction(const char* path) {
    char temp_path[MAX_PATH];
    strncpy(temp_path, path, MAX_PATH);

    // SICHER: Jede Komponente des Pfads prüfen
    char* component = temp_path;
    while (*component) {
        char* next_sep = strchr(component, '\\');
        if (next_sep) {
            *next_sep = '\0';
        }

        if (is_reparse_point(temp_path)) {
            return TRUE;  // Junction im Pfad gefunden
        }

        if (next_sep) {
            *next_sep = '\\';
            component = next_sep + 1;
        } else {
            break;
        }
    }

    return FALSE;
}

// SICHER: Sicheres Dateischreiben mit Junction-Schutz
BOOL safe_write_log(const char* log_dir, const char* message) {
    // SICHER: Auf Junctions im Pfad prüfen
    if (path_contains_junction(log_dir)) {
        SetLastError(ERROR_REPARSE_POINT_ENCOUNTERED);
        return FALSE;
    }

    char log_path[MAX_PATH];
    snprintf(log_path, MAX_PATH, "%s\\app.log", log_dir);

    // SICHER: Mit Flag öffnen, das bei Reparse Points fehlschlägt
    HANDLE hFile = CreateFileA(
        log_path,
        GENERIC_WRITE,
        0,
        NULL,
        OPEN_ALWAYS,
        FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT,  // Reparse erkennen
        NULL
    );

    if (hFile == INVALID_HANDLE_VALUE) {
        return FALSE;
    }

    // SICHER: Prüfen ob Datei ein Reparse Point ist
    BY_HANDLE_FILE_INFORMATION file_info;
    if (GetFileInformationByHandle(hFile, &file_info)) {
        if (file_info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
            CloseHandle(hFile);
            SetLastError(ERROR_REPARSE_POINT_ENCOUNTERED);
            return FALSE;
        }
    }

    DWORD written;
    WriteFile(hFile, message, strlen(message), &written, NULL);
    CloseHandle(hFile);
    return TRUE;
}

// SICHER: Sichere Verzeichnisbereinigung
void safe_cleanup_temp(const char* temp_dir) {
    // SICHER: Prüfen ob temp_dir selbst eine Junction ist
    if (is_reparse_point(temp_dir)) {
        // SICHER: Nur die Junction entfernen, ihr nicht folgen
        RemoveDirectoryA(temp_dir);
        return;
    }

    char search_path[MAX_PATH];
    snprintf(search_path, MAX_PATH, "%s\\*", temp_dir);

    WIN32_FIND_DATAA find_data;
    HANDLE hFind = FindFirstFileA(search_path, &find_data);

    if (hFind != INVALID_HANDLE_VALUE) {
        do {
            if (strcmp(find_data.cFileName, ".") == 0 ||
                strcmp(find_data.cFileName, "..") == 0) {
                continue;
            }

            char file_path[MAX_PATH];
            snprintf(file_path, MAX_PATH, "%s\\%s", temp_dir, find_data.cFileName);

            // SICHER: Auf Reparse Points prüfen
            if (find_data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
                // SICHER: Junction entfernen ohne ihr zu folgen
                RemoveDirectoryA(file_path);
                continue;
            }

            // SICHER: Auf Hard Links prüfen durch Verifikation der Link-Anzahl
            if (!(find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
                HANDLE hFile = CreateFileA(
                    file_path, 0, FILE_SHARE_READ | FILE_SHARE_WRITE,
                    NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
                );

                if (hFile != INVALID_HANDLE_VALUE) {
                    BY_HANDLE_FILE_INFORMATION info;
                    if (GetFileInformationByHandle(hFile, &info)) {
                        if (info.nNumberOfLinks > 1) {
                            // SICHER: Datei ist per Hard Link verknüpft, nicht löschen
                            CloseHandle(hFile);
                            continue;
                        }
                    }
                    CloseHandle(hFile);
                }
            }

            if (find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
                safe_cleanup_temp(file_path);  // Rekursiv mit gleichen Prüfungen
                RemoveDirectoryA(file_path);
            } else {
                DeleteFileA(file_path);
            }

        } while (FindNextFileA(hFind, &find_data));
        FindClose(hFind);
    }
}

// SICHER: Sichere Pfadvalidierung
BOOL validate_path_security(const char* path, const char* allowed_base) {
    char resolved_path[MAX_PATH];
    char resolved_base[MAX_PATH];

    // SICHER: Kanonische Pfade ermitteln
    if (!GetFullPathNameA(path, MAX_PATH, resolved_path, NULL)) {
        return FALSE;
    }
    if (!GetFullPathNameA(allowed_base, MAX_PATH, resolved_base, NULL)) {
        return FALSE;
    }

    // SICHER: Prüfen ob Pfad unter erlaubtem Basispfad liegt
    size_t base_len = strlen(resolved_base);
    if (_strnicmp(resolved_path, resolved_base, base_len) != 0) {
        return FALSE;
    }

    // SICHER: Auf Junctions im Pfad prüfen
    if (path_contains_junction(resolved_path)) {
        return FALSE;
    }

    return TRUE;
}
// SICHER: C#-Code mit Junction/Mount-Point-Schutz

using System;
using System.IO;
using System.Runtime.InteropServices;

public class SafeFileOperations
{
    // SICHER: Prüfen ob Pfad ein Reparse Point ist
    private bool IsReparsePoint(string path)
    {
        FileAttributes attrs = File.GetAttributes(path);
        return (attrs & FileAttributes.ReparsePoint) != 0;
    }

    // SICHER: Gesamten Pfad auf Junctions prüfen
    private bool PathContainsReparsePoint(string path)
    {
        string[] components = path.Split(Path.DirectorySeparatorChar);
        string currentPath = "";

        foreach (string component in components)
        {
            if (string.IsNullOrEmpty(component)) continue;

            currentPath = string.IsNullOrEmpty(currentPath)
                ? component + Path.DirectorySeparatorChar
                : Path.Combine(currentPath, component);

            if (Directory.Exists(currentPath) && IsReparsePoint(currentPath))
            {
                return true;
            }
        }

        return false;
    }

    // SICHER: Sicheres Dateischreiben mit Junction-Schutz
    public void SafeWriteData(string directory, string data)
    {
        // SICHER: Validieren dass Pfad keine Junctions enthält
        if (PathContainsReparsePoint(directory))
        {
            throw new SecurityException("Pfad enthält Reparse Point");
        }

        string filePath = Path.Combine(directory, "data.txt");

        // SICHER: Prüfen ob Ziel nicht als Reparse Point existiert
        if (File.Exists(filePath) && IsReparsePoint(filePath))
        {
            throw new SecurityException("Ziel ist ein Reparse Point");
        }

        File.WriteAllText(filePath, data);
    }

    // SICHER: Sichere Verzeichnislöschung
    public void SafeCleanupDirectory(string path)
    {
        // SICHER: Wenn oberste Ebene eine Junction ist, nur die Junction entfernen
        if (IsReparsePoint(path))
        {
            Directory.Delete(path, false);  // Nur Junction entfernen
            return;
        }

        // SICHER: Manuell iterieren ohne Junctions zu folgen
        foreach (string file in Directory.GetFiles(path))
        {
            // SICHER: Auf Hard Links prüfen
            FileInfo fi = new FileInfo(file);
            if (IsHardLink(file))
            {
                // Per Hard Link verknüpfte Dateien überspringen
                continue;
            }
            File.Delete(file);
        }

        foreach (string dir in Directory.GetDirectories(path))
        {
            if (IsReparsePoint(dir))
            {
                // SICHER: Junction entfernen ohne ihr zu folgen
                Directory.Delete(dir, false);
            }
            else
            {
                SafeCleanupDirectory(dir);  // Rekursiv mit gleichen Prüfungen
                Directory.Delete(dir, false);
            }
        }
    }

    // SICHER: Prüfen ob Datei ein Hard Link ist
    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    private static extern bool GetFileInformationByHandle(
        IntPtr hFile, out BY_HANDLE_FILE_INFORMATION lpFileInformation);

    [StructLayout(LayoutKind.Sequential)]
    private struct BY_HANDLE_FILE_INFORMATION
    {
        public uint FileAttributes;
        public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime;
        public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime;
        public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime;
        public uint VolumeSerialNumber;
        public uint FileSizeHigh;
        public uint FileSizeLow;
        public uint NumberOfLinks;
        public uint FileIndexHigh;
        public uint FileIndexLow;
    }

    private bool IsHardLink(string path)
    {
        using (FileStream fs = File.Open(path, FileMode.Open,
                                         FileAccess.Read, FileShare.ReadWrite))
        {
            BY_HANDLE_FILE_INFORMATION info;
            if (GetFileInformationByHandle(fs.SafeFileHandle.DangerousGetHandle(), out info))
            {
                return info.NumberOfLinks > 1;
            }
        }
        return false;
    }

    // SICHER: Prüfen ob Pfad innerhalb des erlaubten Verzeichnisses bleibt
    public bool ValidatePathSecurity(string path, string allowedBase)
    {
        string fullPath = Path.GetFullPath(path);
        string fullBase = Path.GetFullPath(allowedBase);

        // SICHER: Sicherstellen dass Pfad unter erlaubter Basis liegt
        if (!fullPath.StartsWith(fullBase, StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }

        // SICHER: Auf Junctions im Pfad prüfen
        if (PathContainsReparsePoint(fullPath))
        {
            return false;
        }

        return true;
    }
}

CVE-Beispiele

  • CVE-2020-0668: Windows Service Tracing beliebiges Dateischreiben durch Junction.
  • CVE-2020-1088: Windows-Fehlerberichterstattung Rechteeskalation über Symlink.
  • CVE-2019-1069: Windows Task Scheduler Dateiüberschreibung durch Hard Link.

Verwandte CWEs

  • CWE-59: Improper Link Resolution Before File Access (übergeordnet)
  • CWE-362: Race Condition (verwandt)
  • CWE-367: Time-of-Check Time-of-Use (TOCTOU) Race Condition (verwandt)

Referenzen

  1. MITRE Corporation. "CWE-1386: Insecure Operation on Windows Junction / Mount Point." https://cwe.mitre.org/data/definitions/1386.html
  2. Microsoft. "Hard Links and Junctions"
  3. Project Zero. "Windows Symbolic Link Attacks"