跳转至

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

ZIP 漏洞:路径遍历、Zip 符号链接和 Zip 扩展名欺骗

描述

ZIP 文件是一种用于存储和传输多个文件的压缩归档文件。由于其便利性和跨不同平台的兼容性,它是一种被广泛采用的文件格式。然而,与任何数字文件格式一样,ZIP 文件也无法免受漏洞的影响。以下是一些与 ZIP 文件相关的常见漏洞:

路径遍历 (Path Traversal)

路径遍历(也称为目录遍历或目录攀爬)是一种漏洞,它允许攻击者访问预期提取目录之外的文件或目录。在提取 ZIP 文件时,如果提取过程未正确验证归档文件中的文件路径,攻击者可以制作一个包含特殊字符或序列的恶意 ZIP 文件,使他们能够遍历目录并访问系统上的敏感文件。这可能导致敏感信息的未经授权披露,甚至导致远程代码执行。

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()
}

Zip 符号链接

符号链接(或 symlinks)是指向文件或目录的指针,可用于创建快捷方式或引用。然而,如果 ZIP 文件提取过程未能正确处理符号链接,攻击者可以制作一个包含指向目标系统上敏感文件或目录的符号链接的恶意 ZIP 文件。在提取时,这些符号链接可能会被跟踪,从而导致对关键文件或目录的未经授权访问。

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()
}

Zip 扩展名欺骗

Zip 扩展名欺骗是一种技术,攻击者通过欺骗 ZIP 文件中恶意文件的扩展名来欺骗用户和安全系统。通过操纵 ZIP 文件头,攻击者可以更改归档文件中文件的扩展名,使其看起来无害。然而,当用户提取该文件或使用存在漏洞的应用程序打开它时,恶意有效载荷将被执行,可能导致未经授权的代码执行、恶意软件感染或其他恶意活动。

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()
}

建议

为了缓解与 zip 文件相关的风险,请考虑以下事项:

  • 对于要提取的每个 zip 条目,使用标准库标准化路径,并检查其是否包含在提取目录中。
  • 实施正确的输入验证和清理,以防止用户提供的输入包含目录遍历序列。
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
}

Zip 符号链接

  • 在提取文件之前,检查 ZIP 归档文件中的符号链接,并确保在提取过程中不会盲目跟踪它们。
  • 验证并清理符号链接目标,以防止目录遍历或访问敏感系统文件。
  • 使用平台特定的功能或库来安全地处理符号链接并防止创建恶意链接。
  • 将提取过程限制在已知安全的位置,并避免允许在这些边界之外创建符号链接。
  • 忽略符号链接 (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
}

Zip 扩展名欺骗

  • 对提取的文件执行额外的检查或验证,以确保其真实的文件类型与预期的扩展名匹配。
  • 考虑使用文件签名或魔数 (magic numbers) 来验证文件的内容,并将其与指示的扩展名进行比较。
  • 实施基于扩展名和文件头的文件类型验证,以确保一致性。
  • 考虑使用专门设计用于安全处理 ZIP 文件的第三方库或工具,因为它们可能提供针对扩展名欺骗攻击的内置保护。
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
}

链接

标准

  • 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