Saltar a contenido

ZIP Vulnerabilities: Path Traversal, Zip Symbolic Link, and Zip Extension Spoofing

Vulnerabilidades ZIP: Path Traversal, Enlaces simbólicos y Falsificación de extensión

Descripción

Los archivos ZIP, que son archivos comprimidos utilizados para almacenar y transmitir múltiples archivos, son un formato de archivo ampliamente adoptado debido a su conveniencia y compatibilidad en diferentes plataformas. Sin embargo, como cualquier formato de archivo digital, los archivos ZIP no son inmunes a las vulnerabilidades. A continuación se presentan algunas vulnerabilidades comunes asociadas a los archivos ZIP:

Path Traversal (Salto de directorio)

El salto de directorio, también conocido como "directory traversal" o escalada de directorios, es una vulnerabilidad que permite a un atacante acceder a archivos o directorios fuera del directorio de extracción previsto. Al extraer un archivo ZIP, si el proceso de extracción no valida adecuadamente las rutas de los archivos dentro del archivo, un atacante puede crear un archivo ZIP malicioso que contenga secuencias o caracteres especiales que le permitan atravesar directorios y acceder a archivos confidenciales del sistema. Esto puede conducir a la divulgación no autorizada de información confidencial o incluso a la ejecución remota de código.

import 'dart:io';
import 'archive/archive.dart';

void extractZipFile(String path) {
  File file = File(path);
  Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());

  for (ArchiveFile archiveFile in archive) {
    // Insecure: Does not properly validate file paths
    File extractedFile = File('/tmp/' + archiveFile.name);
    extractedFile.createSync(recursive: true);
    extractedFile.writeAsBytesSync(archiveFile.content);
  }
}
import Foundation
import ZIPFoundation

func extractZipFile(path: String) {
    guard let archive = Archive(url: URL(fileURLWithPath: path), accessMode: .read) else {
        return
    }

    for entry in archive {
        // Insecure: Does not properly validate file paths
        let extractedFilePath = "/tmp/\(entry.path)"
        let extractedFileURL = URL(fileURLWithPath: extractedFilePath)

        do {
            try FileManager.default.createDirectory(atPath: extractedFileURL.deletingLastPathComponent().path,
                                                    withIntermediateDirectories: true,
                                                    attributes: nil)
            try archive.extract(entry, to: extractedFileURL)
        } catch {
            print("Extraction failed: \(error.localizedDescription)")
        }
    }
}
import java.io.File
import java.util.zip.ZipInputStream

fun extractZipFile(path: String) {
    val file = File(path)
    val zipInputStream = ZipInputStream(file.inputStream())

    var entry = zipInputStream.nextEntry
    while (entry != null) {
        // Insecure: Does not properly validate file paths
        val extractedFile = File("/tmp/" + entry.name)
        extractedFile.parentFile.mkdirs()
        extractedFile.outputStream().use { output ->
            zipInputStream.copyTo(output)
        }

        entry = zipInputStream.nextEntry
    }

    zipInputStream.close()
}

Enlace simbólico ZIP

Los enlaces simbólicos, o symlinks, son punteros a archivos o directorios que pueden usarse para crear accesos directos o referencias. Sin embargo, si un proceso de extracción de archivos ZIP no maneja correctamente los enlaces simbólicos, un atacante puede manipular un archivo ZIP malicioso que incluya enlaces simbólicos apuntando a archivos o directorios confidenciales en el sistema de destino. Tras la extracción, estos enlaces simbólicos pueden seguirse, lo que da lugar a un acceso no autorizado a archivos o directorios críticos.

import 'dart:io';
import 'archive/archive.dart';

void extractZipFile(String path) {
  File file = File(path);
  Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());

  for (ArchiveFile archiveFile in archive) {
    // Insecure: Does not handle symbolic links properly
    if (archiveFile.isSymbolicLink) {
      File symlink = File('/tmp/' + archiveFile.name);
      symlink.createSync(recursive: true);
      symlink.writeAsStringSync(archiveFile.content);
    } else {
      File extractedFile = File('/tmp/' + archiveFile.name);
      extractedFile.createSync(recursive: true);
      extractedFile.writeAsBytesSync(archiveFile.content);
    }
  }
}
import Foundation
import ZIPFoundation

func extractZipFile(path: String) {
    guard let archive = Archive(url: URL(fileURLWithPath: path), accessMode: .read) else {
        return
    }

    for entry in archive {
        // Insecure: Does not handle symbolic links properly
        let extractedFilePath = "/tmp/\(entry.path)"
        let extractedFileURL = URL(fileURLWithPath: extractedFilePath)

        if entry.type == .symbolicLink {
            do {
                try FileManager.default.createSymbolicLink(at: extractedFileURL, withDestinationURL: entry.destinationURL)
            } catch {
                print("Symbolic link creation failed: \(error.localizedDescription)")
            }
        } else {
            do {
                try FileManager.default.createDirectory(atPath: extractedFileURL.deletingLastPathComponent().path,
                                                        withIntermediateDirectories: true,
                                                        attributes: nil)
                try archive.extract(entry, to: extractedFileURL)
            } catch {
                print("Extraction failed: \(error.localizedDescription)")
            }
        }
    }
}
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.util.zip.ZipInputStream

fun extractZipFile(path: String) {
    val file = File(path)
    val zipInputStream = ZipInputStream(file.inputStream())

    var entry = zipInputStream.nextEntry
    while (entry != null) {
        // Insecure: Does not handle symbolic links properly
        val extractedFile = File("/tmp/" + entry.name)
        if (entry.isSymbolicLink) {
            Files.createSymbolicLink(Path.of(extractedFile.path), Path.of(entry.link))
        } else {
            extractedFile.parentFile.mkdirs()
            extractedFile.outputStream().use { output ->
                zipInputStream.copyTo(output)
            }
        }

        entry = zipInputStream.nextEntry
    }

    zipInputStream.close()
}

Falsificación de extensión ZIP

La falsificación de extensión ZIP es una técnica en la que un atacante falsifica la extensión de un archivo malicioso dentro de un archivo ZIP para engañar a los usuarios y a los sistemas de seguridad. Al manipular las cabeceras del archivo ZIP, el atacante puede cambiar la extensión del archivo dentro del archivo para que parezca inofensivo. Sin embargo, cuando el usuario extrae el archivo o lo abre con una aplicación vulnerable, se ejecuta la carga maliciosa, lo que puede provocar la ejecución de código no autorizado, la infección de malware u otras actividades maliciosas.

import 'dart:io';
import 'archive/archive.dart';

void extractZipFile(String path) {
  File file = File(path);
  Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());

  for (ArchiveFile archiveFile in archive) {
    // Insecure: the zip decoder parses the filename from the Local File Header which can be manipulated
    // Zipdecoder has to check against the central directory to make sure the extension was not altered
    String extractedFilePath = '/tmp/' + archiveFile.name;
    if (archiveFile.name.endsWith('.zip')) {
      // Extracting a ZIP file within a ZIP file
      extractZipFile(extractedFilePath);
    } else {
      File extractedFile = File(extractedFilePath);
      extractedFile.createSync(recursive: true);
      extractedFile.writeAsBytesSync(archiveFile.content);
    }
  }
}
import java.io.File
import java.util.zip.ZipInputStream

fun extractZipFile(path: String) {
    val file = File(path)
    val zipInputStream = ZipInputStream(file.inputStream())

    var entry = zipInputStream.nextEntry
    while (entry != null) {
        // Insecure: the zip decoder parses the filename from the Local File Header which can be altered
        // Zipdecoder has to check against the central directory to make sure the extension was not altered
        val extractedFile = File("/tmp/" + entry.name)
        if (entry.name.endsWith(".zip")) {
            // Extracting a ZIP file within a ZIP file
            extractZipFile(extractedFile.path)
        } else {
            extractedFile.parentFile.mkdirs()
            extractedFile.outputStream().use { output ->
                zipInputStream.copyTo(output)
            }
        }

        entry = zipInputStream.nextEntry
    }

    zipInputStream.close()
}

Recomendación

Para mitigar los riesgos asociados con los archivos zip, tenga en cuenta lo siguiente:

  • Por cada entrada zip a extraer, estandarice la ruta utilizando una biblioteca estándar y compruebe si está contenida en el directorio de extracción.
  • Implemente una validación y saneamiento de entrada adecuados para evitar que la entrada proporcionada por el usuario contenga secuencias de salto de directorio.
import 'dart:io';
import 'archive/archive.dart';

void extractZipFile(String path) {
  File file = File(path);
  Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());

  for (ArchiveFile archiveFile in archive) {
    String extractedFilePath = '/tmp/' + sanitizeFilePath(archiveFile.name);

    // Check if the extracted file path is within the allowed directory
    if (isPathWithinAllowedDirectory(extractedFilePath)) {
      File extractedFile = File(extractedFilePath);
      extractedFile.createSync(recursive: true);
      extractedFile.writeAsBytesSync(archiveFile.content);
    }
  }
}

String sanitizeFilePath(String filePath) {
  // Implement logic to sanitize the file path and remove any potentially harmful characters or sequences
  // Return the sanitized file path
}

bool isPathWithinAllowedDirectory(String filePath) {
  // Implement logic to check if the extracted file path is within the allowed directory
  // Return true if the file path is allowed, false otherwise
}
import Foundation

func extractZipFile(path: String) {
    guard let archive = Archive(url: URL(fileURLWithPath: path), accessMode: .read) else {
        return
    }

    let destinationDir = URL(fileURLWithPath: "/tmp/")

    for entry in archive {
        let extractedFilePath = sanitizeFilePath(entry.path)

        // Check if the extracted file path is within the allowed directory
        if isPathWithinAllowedDirectory(extractedFilePath) {
            let extractedFileURL = destinationDir.appendingPathComponent(extractedFilePath)

            do {
                try archive.extract(entry, to: extractedFileURL)
            } catch {
                print("Error extracting file: \(error)")
            }
        }
    }
}

func sanitizeFilePath(_ filePath: String) -> String {
    // Implement logic to sanitize the file path and remove any potentially harmful characters or sequences
    // Return the sanitized file path
}

func isPathWithinAllowedDirectory(_ filePath: String) -> Bool {
    // Implement logic to check if the extracted file path is within the allowed directory
    // Return true if the file path is allowed, false otherwise
}
import java.io.File
import java.util.zip.ZipEntry
import java.util.zip.ZipFile

fun extractZipFile(path: String) {
    val file = File(path)
    val zipFile = ZipFile(file)

    val destinationDir = File("/tmp/")
    val zipEntries = zipFile.entries()

    while (zipEntries.hasMoreElements()) {
        val zipEntry = zipEntries.nextElement()
        val extractedFilePath = sanitizeFilePath(zipEntry.name)

        // Check if the extracted file path is within the allowed directory
        if (isPathWithinAllowedDirectory(extractedFilePath)) {
            val extractedFile = File(destinationDir, extractedFilePath)
            extractedFile.parentFile.mkdirs()
            extractedFile.outputStream().use { outputStream ->
                zipFile.getInputStream(zipEntry).copyTo(outputStream)
            }
        }
    }
    zipFile.close()
}

fun sanitizeFilePath(filePath: String): String {
    // Implement logic to sanitize the file path and remove any potentially harmful characters or sequences
    // Return the sanitized file path
}

fun isPathWithinAllowedDirectory(filePath: String): Boolean {
    // Implement logic to check if the extracted file path is within the allowed directory
    // Return true if the file path is allowed, false otherwise
}

Enlace simbólico ZIP

  • Antes de extraer archivos, verifique la existencia de enlaces simbólicos dentro del archivo ZIP y asegúrese de que no se sigan ciegamente durante la extracción.
  • Valide y sanee el destino del enlace simbólico para evitar el salto de directorios o el acceso a archivos confidenciales del sistema.
  • Utilice bibliotecas o funciones específicas de la plataforma que manejen los enlaces simbólicos de forma segura y eviten la creación de enlaces maliciosos.
  • Limite el proceso de extracción a ubicaciones seguras conocidas y evite que se creen enlaces simbólicos fuera de esos límites.
  • Ignore los enlaces simbólicos (symlinks).
import 'dart:io';
import 'archive/archive.dart';
import 'path';

void extractZipFile(String path) {
  File file = File(path);
  Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());

  for (ArchiveFile archiveFile in archive) {
    if (!isSymbolicLink(archiveFile)) {
      // Extract regular file
      String extractedFilePath = '/tmp/' + sanitizePath(archiveFile.name);
      File extractedFile = File(extractedFilePath);
      extractedFile.createSync(recursive: true);
      extractedFile.writeAsBytesSync(archiveFile.content);
    } 
  }
}

bool isSymbolicLink(ArchiveFile archiveFile) {
  // Implement platform-specific logic to check if the file is a symbolic link
  // Return true if it is a symbolic link, false otherwise
} 
import Foundation
import ZIPFoundation

func extractZipFile(path: String) {
    let fileManager = FileManager.default
    guard let archive = Archive(url: URL(fileURLWithPath: path), accessMode: .read) else {
        return
    }

    for entry in archive {
        if !isSymbolicLink(entry) {
            // Extract regular file
            let extractedFilePath = "/tmp/" + sanitizePath(entry.path)
            let extractedFileURL = URL(fileURLWithPath: extractedFilePath)
            fileManager.createFile(atPath: extractedFilePath, contents: entry.data, attributes: nil)
        } 
    }
}

func isSymbolicLink(_ entry: Entry) -> Bool {
    // Implement platform-specific logic to check if the entry is a symbolic link
    // Return true if it is a symbolic link, false otherwise
}
import java.io.File
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.attribute.PosixFilePermission
import java.util.zip.ZipInputStream

fun extractZipFile(path: String) {
    val file = File(path)
    val zipInput = ZipInputStream(file.inputStream())
    var entry = zipInput.nextEntry
    while (entry != null) {
        if (!isSymbolicLink(entry)) {
            // Extract regular file
            val extractedFilePath = File("/tmp", sanitizePath(entry.name))
            extractedFilePath.parentFile.mkdirs()
            Files.copy(zipInput, extractedFilePath.toPath(), StandardCopyOption.REPLACE_EXISTING)
        } 
        entry = zipInput.nextEntry
    }
}

fun isSymbolicLink(entry: ZipEntry): Boolean {
    // Implement platform-specific logic to check if the entry is a symbolic link
    // Return true if it is a symbolic link, false otherwise
}

Falsificación de extensión ZIP

  • Realice comprobaciones o validaciones adicionales en los archivos extraídos para garantizar que su verdadero tipo de archivo coincida con la extensión esperada.
  • Considere utilizar firmas de archivos o números mágicos ("magic numbers") para verificar el contenido del archivo y compararlo con la extensión indicada.
  • Implemente la verificación del tipo de archivo en función tanto de la extensión como de la cabecera del archivo para asegurar la consistencia.
  • Considere la posibilidad de utilizar herramientas o bibliotecas de terceros diseñadas específicamente para procesar archivos ZIP de forma segura, ya que pueden ofrecer protección integrada contra los ataques de falsificación de extensiones.
import 'dart:io';
import 'archive/archive.dart';
import 'path';

void extractZipFile(String path) {
  File file = File(path);
  Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());

  for (ArchiveFile archiveFile in archive) {
    // Mitigation: Validate the file type by comparing the extension and file header
    if (isFileExtensionValid(archiveFile) && isFileHeaderValid(archiveFile)) {
      String extractedFilePath = '/tmp/' + sanitizePath(archiveFile.name);
      File extractedFile = File(extractedFilePath);
      extractedFile.createSync(recursive: true);
      extractedFile.writeAsBytesSync(archiveFile.content);
    } else {
      // Handle case when file type does not match the expected extension
      print('Invalid file type detected: ${archiveFile.name}');
    }
  }
}

bool isFileExtensionValid(ArchiveFile archiveFile) {
  // Implement logic to validate the file extension against expected extensions
  // Return true if the file extension is valid, false otherwise
}

bool isFileHeaderValid(ArchiveFile archiveFile) {
  // Implement logic to validate the file header and ensure it matches the expected file type
  // Return true if the file header is valid, false otherwise
}
import Foundation
import ZIPFoundation

func extractZipFile(path: String) {
    let fileManager = FileManager.default
    guard let archive = Archive(url: URL(fileURLWithPath: path), accessMode: .read) else {
        return
    }

    for entry in archive {
        // Mitigation: Validate the file type by comparing the extension and file header
        if isFileExtensionValid(entry) && isFileHeaderValid(entry) {
            let extractedFilePath = "/tmp/" + sanitizePath(entry.path)
            let extractedFileURL = URL(fileURLWithPath: extractedFilePath)
            fileManager.createFile(atPath: extractedFilePath, contents: entry.data, attributes: nil)
        } else {
            // Handle case when file type does not match the expected extension
            print("Invalid file type detected: \(entry.path)")
        }
    }
}

func isFileExtensionValid(_ entry: Entry) -> Bool {
    // Implement logic to validate the file extension against expected extensions
    // Return true if the file extension is valid, false otherwise
}

func isFileHeaderValid(_ entry: Entry) -> Bool {
    // Implement logic to validate the file header and ensure it matches the expected file type
    // Return true if the file header is valid, false otherwise
}
import java.io.File
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.attribute.PosixFilePermission
import java.util.zip.ZipInputStream

fun extractZipFile(path: String) {
    val file = File(path)
    val zipInput = ZipInputStream(file.inputStream())
    var entry = zipInput.nextEntry
    while (entry != null) {
        // Mitigation: Validate the file type by comparing the extension and file header
        if (isFileExtensionValid(entry) && isFileHeaderValid(entry)) {
            val extractedFilePath = File("/tmp", sanitizePath(entry.name))
            extractedFilePath.parentFile.mkdirs()
            Files.copy(zipInput, extractedFilePath.toPath(), StandardCopyOption.REPLACE_EXISTING)
        } else {
            // Handle case when file type does not match the expected extension
            println("Invalid file type detected: ${entry.name}")
        }
        entry = zipInput.nextEntry
    }
}

fun isFileExtensionValid(entry: ZipEntry): Boolean {
    // Implement logic to validate the file extension against expected extensions
    // Return true if the file extension is valid, false otherwise
}

fun isFileHeaderValid(entry: ZipEntry): Boolean {
    // Implement logic to validate the file header and ensure it matches the expected file type
    // Return true if the file header is valid, false otherwise
}

Enlaces

Estándares

  • OWASP_MASVS_L1:
    • MSTG_PLATFORM_2
  • OWASP_MASVS_L2:
    • MSTG_PLATFORM_2
  • PCI_STANDARDS:
    • REQ_2_2
    • REQ_6_2
    • REQ_6_3
    • REQ_11_3
  • OWASP_MASVS_v2_1:
    • MASVS_CODE_4
  • SOC2_CONTROLS:
    • CC_2_1
    • CC_4_1
    • CC_7_1
    • CC_7_2
    • CC_7_4
    • CC_7_5
  • HIPAA_CONTROLS:
    • SECURITY212
    • SECURITY213
    • SECURITY255