那些默认不安全的ZIP Slip攻击
字数 1643 2025-08-22 12:23:19

ZIP Slip 压缩目录穿越攻击全面解析与防御指南

1. ZIP Slip 攻击概述

ZIP Slip(压缩目录穿越攻击)是一种针对处理压缩文件(如ZIP、TAR等格式)应用程序的安全漏洞。攻击者通过精心构造的压缩文件,诱导目标应用程序在解压时将文件提取到预期目录之外的位置,可能导致:

  • 覆盖重要系统文件
  • 执行恶意代码
  • 破坏系统正常运行

2. 攻击原理详解

攻击核心在于路径遍历序列的利用:

  1. 攻击者构造包含特殊路径的压缩文件(如使用"../"表示上级目录)
  2. 应用程序解压时未对路径进行严格验证
  3. 文件被解压到预期目录之外的位置

典型攻击场景

  • Web应用允许用户上传并解压文件到"/uploads"目录
  • 攻击者上传包含路径为"../../etc/passwd"的压缩文件
  • 应用解压时覆盖系统"/etc/passwd"文件

3. 各语言不安全实现分析

3.1 Java 不安全实现

3.1.1 ZipInputStream

public static void unsafe_unzip(String file_name, String output) {
    File destDir = new File(output);
    try (ZipInputStream zip = new ZipInputStream(new FileInputStream(file_name))) {
        ZipEntry entry;
        while ((entry = zip.getNextEntry()) != null) {
            String path = output + File.separator + entry.getName();
            try (FileOutputStream fos = new FileOutputStream(path)) {
                byte[] buffer = new byte[1024];
                int len;
                while ((len = zip.read(buffer)) > 0) {
                    fos.write(buffer, 0, len);
                }
            }
            zip.closeEntry();
        }
    } catch (IOException e) {}
}

漏洞点

  • 直接使用entry.getName()构建路径
  • 未对路径进行清理或验证

3.1.2 ZipFile

public static void unsafe_unzip5(String file_name, String output) {
    try (ZipFile zipFile = new ZipFile(new File(file_name))) {
        zipFile.entries().asIterator().forEachRemaining(entry -> {
            try {
                Path destPath = Paths.get(output, entry.getName());
                File fil = new File(destPath.toString());
                if (entry.isDirectory()) {
                    fil.mkdirs();
                } else {
                    fil.getParentFile().mkdirs();
                    try (InputStream in = zipFile.getInputStream(entry);
                         OutputStream out = Files.newOutputStream(destPath)) {
                        in.transferTo(out);
                    }
                }
            } catch (IOException e){}
        });
    } catch (IOException e) {
        e.printStackTrace();
    }
}

漏洞点

  • 直接使用entry.getName()构建目标路径
  • 未检查文件名中的"../"等遍历字符

3.1.3 TarArchiveInputStream

public static void unsafe_untar(String file_name, String output) {
    File destDir = new File(output);
    try (TarArchiveInputStream tarIn = new TarArchiveInputStream(
            new BufferedInputStream(new FileInputStream(file_name)))){
        ArchiveEntry entry;
        while ((entry = tarIn.getNextEntry()) != null){
            Path extractTo = Paths.get(output).resolve(entry.getName());
            Files.copy(tarIn, extractTo);
        }
    } catch (IOException e){
        e.printStackTrace();
    }
}

漏洞点

  • 直接使用entry.getName()解析路径
  • 未对路径进行规范化处理

3.2 Python 不安全实现

3.2.1 ZipFile + shutil.copyfileobj()

def unzip(file_name, output): # bad
    with zipfile.ZipFile(file_name, 'r') as zf:
        for filename in zf.namelist():
            # Output
            output_path = os.path.join(output, filename)
            with zf.open(filename) as source:
                with open(output_path, 'wb') as destination:
                    shutil.copyfileobj(source, destination)

漏洞点

  • os.path.join未规范化路径
  • 允许文件名中包含"../"引用父目录

3.2.2 TarFile.extract()

def untar(file_name, output): # bad
    with tarfile.open(file_name, 'r') as tf:
        for member in tf.getmembers():
            tf.extract(member)

漏洞点

  • extract()方法不会自动移除多余的分隔符和点
  • 直接使用压缩包中的路径信息

3.3 Golang 不安全实现

3.3.1 archive/zip

func unzip(src string, dest string) error {
    r, err := zip.OpenReader(src)
    if err != nil {
        return err
    }
    defer r.Close()
    
    for _, f := range r.File {
        fpath := filepath.Join(dest, f.Name)
        if f.FileInfo().IsDir() {
            os.MkdirAll(fpath, os.ModePerm)
            continue
        }
        
        outFile, err := os.Create(fpath)
        if err != nil {
            return err
        }
        defer outFile.Close()
        
        inFile, err := f.Open()
        if err != nil {
            return err
        }
        defer inFile.Close()
        
        _, err = io.Copy(outFile, inFile)
        if err != nil {
            return err
        }
    }
    return nil
}

漏洞点

  • 直接使用f.Name构建路径
  • 缺少路径安全验证

3.3.2 archive/tar

func untar(src string, dest string) error {
    file, err := os.Open(src)
    if err != nil {
        return err
    }
    defer file.Close()
    
    tr := tar.NewReader(file)
    for {
        header, err := tr.Next()
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }
        
        target := filepath.Join(dest, header.Name)
        if header.Typeflag == tar.TypeDir {
            os.MkdirAll(target, os.ModePerm)
            continue
        }
        
        outFile, err := os.Create(target)
        if err != nil {
            return err
        }
        defer outFile.Close()
        
        _, err = io.Copy(outFile, tr)
        if err != nil {
            return err
        }
    }
    return nil
}

漏洞点

  • 直接使用header.Name构建目标路径
  • 缺少路径安全验证

3.4 Ruby 不安全实现

3.4.1 zip模块

def unsafe_unzip(file_name, output) # bad
    Zip::File.open(file_name) do |zip_file|
        zip_file.each do |entry|
            file_path = File.join(output, entry.name)
            FileUtils.mkdir_p(File.dirname(file_path))
            zip_file.extract(entry, file_path)
        end
    end
end

漏洞点

  • extract()方法不会自动处理多余的分隔符和点
  • 直接使用entry.name构建路径

3.4.2 TarReader模块

def unsafe_untar(file_name, output) # bad
    File.open(file_name, 'rb') do |file_stream|
        Gem::Package::TarReader.new(file_stream).each do |entry|
            entry_var = entry.full_name
            path = File.expand_path(entry_var, output)
            File.open(path, 'wb') do |f|
                f.write(entry.read)
            end
        end
    end
end

漏洞点

  • 直接使用entry.full_name构建路径
  • 缺少路径规范化处理

4. 构造恶意压缩文件的方法

4.1 ZIP类型

import zipfile

def compress_file(filename):
    with zipfile.ZipFile('../payloads/payload.zip', 'w') as zipf:
        zipf.writestr(filename, "PoC")

filename = '../poc.txt'
compress_file(filename)

4.2 TAR类型

import tarfile
import io

def compress_file(filename):
    with tarfile.open('../payloads/payload.tar', 'w') as tarf:
        data = io.BytesIO(b"Test payload")
        tarinfo = tarfile.TarInfo(name=filename)
        tarinfo.size = len(data.getvalue())
        tarf.addfile(tarinfo, data)

filename = '../poc.txt'
compress_file(filename)

4.3 TAR.GZ类型

import tarfile
import io

def create_tar_gz_archive(output_tar_gz_file, file_name, content):
    with tarfile.open(output_tar_gz_file, 'w:gz') as tar:
        file_like_object = io.BytesIO(content.encode('utf-8'))
        tarinfo = tarfile.TarInfo(name=file_name)
        tarinfo.size = len(content)
        tar.addfile(tarinfo, file_like_object)

create_tar_gz_archive('../payloads/payload.tar.gz', '../poc.txt', 'PoC')

5. 安全防御措施

5.1 通用防御原则

  1. 路径规范化:使用规范化函数处理所有路径
  2. 路径验证:检查解压路径是否在目标目录内
  3. 白名单验证:只允许特定文件类型或名称模式
  4. 权限最小化:使用最低必要权限运行解压操作

5.2 各语言安全实现示例

5.2.1 Java安全实现

public static void safe_unzip(String file_name, String output) throws IOException {
    Path destDir = Paths.get(output).normalize().toAbsolutePath();
    
    try (ZipInputStream zip = new ZipInputStream(new FileInputStream(file_name))) {
        ZipEntry entry;
        while ((entry = zip.getNextEntry()) != null) {
            Path destPath = destDir.resolve(entry.getName()).normalize();
            
            // 验证路径是否在目标目录内
            if (!destPath.startsWith(destDir)) {
                throw new IOException("Invalid entry: " + entry.getName());
            }
            
            if (entry.isDirectory()) {
                Files.createDirectories(destPath);
            } else {
                Files.createDirectories(destPath.getParent());
                try (OutputStream fos = Files.newOutputStream(destPath)) {
                    byte[] buffer = new byte[1024];
                    int len;
                    while ((len = zip.read(buffer)) > 0) {
                        fos.write(buffer, 0, len);
                    }
                }
            }
            zip.closeEntry();
        }
    }
}

5.2.2 Python安全实现

def safe_unzip(file_name, output):
    output = os.path.abspath(output)
    with zipfile.ZipFile(file_name, 'r') as zf:
        for filename in zf.namelist():
            # 规范化路径并确保在目标目录内
            file_path = os.path.normpath(os.path.join(output, filename))
            if not file_path.startswith(output):
                raise ValueError(f"Path traversal attempt: {filename}")
            
            if filename.endswith('/'):
                os.makedirs(file_path, exist_ok=True)
            else:
                os.makedirs(os.path.dirname(file_path), exist_ok=True)
                with zf.open(filename) as source, open(file_path, 'wb') as destination:
                    shutil.copyfileobj(source, destination)

5.2.3 Golang安全实现

func safe_unzip(src string, dest string) error {
    dest = filepath.Clean(dest)
    if !filepath.IsAbs(dest) {
        return fmt.Errorf("destination path must be absolute")
    }

    r, err := zip.OpenReader(src)
    if err != nil {
        return err
    }
    defer r.Close()

    for _, f := range r.File {
        // 清理路径并验证
        fpath := filepath.Join(dest, f.Name)
        fpath = filepath.Clean(fpath)
        
        // 检查路径是否在目标目录内
        if !strings.HasPrefix(fpath, dest) {
            return fmt.Errorf("illegal file path: %s", f.Name)
        }

        if f.FileInfo().IsDir() {
            os.MkdirAll(fpath, os.ModePerm)
            continue
        }

        if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
            return err
        }

        outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
        if err != nil {
            return err
        }

        rc, err := f.Open()
        if err != nil {
            outFile.Close()
            return err
        }

        _, err = io.Copy(outFile, rc)
        outFile.Close()
        rc.Close()
        if err != nil {
            return err
        }
    }
    return nil
}

5.2.4 Ruby安全实现

def safe_unzip(file_name, output)
  output = File.expand_path(output)
  
  Zip::File.open(file_name) do |zip_file|
    zip_file.each do |entry|
      # 清理路径并验证
      file_path = File.join(output, entry.name)
      file_path = File.expand_path(file_path)
      
      unless file_path.start_with?(output + '/')
        raise "Path traversal attempt: #{entry.name}"
      end
      
      FileUtils.mkdir_p(File.dirname(file_path))
      zip_file.extract(entry, file_path) unless File.exist?(file_path)
    end
  end
end

6. 最佳实践总结

  1. 始终验证解压路径:确保解压路径在目标目录内
  2. 使用绝对路径:避免相对路径带来的不确定性
  3. 规范化所有路径:使用语言提供的规范化函数
  4. 限制文件权限:使用最低必要权限运行解压操作
  5. 考虑使用安全库:如Python的zipfile模块的安全方法
  6. 输入验证:对上传的压缩文件进行严格验证
  7. 日志记录:记录解压操作以便审计
  8. 沙箱环境:在高风险场景下考虑在沙箱中执行解压

7. 测试与验证

实施防御措施后,应进行以下测试:

  1. 路径遍历测试:尝试上传包含"../"的压缩文件
  2. 符号链接测试:测试对符号链接的处理
  3. 绝对路径测试:测试包含绝对路径的压缩文件
  4. 嵌套路径测试:测试深度嵌套的路径
  5. 性能测试:确保安全措施不影响正常功能

通过全面理解和实施这些安全措施,可以有效防御ZIP Slip攻击,保护应用程序免受目录穿越漏洞的威胁。

ZIP Slip 压缩目录穿越攻击全面解析与防御指南 1. ZIP Slip 攻击概述 ZIP Slip(压缩目录穿越攻击)是一种针对处理压缩文件(如ZIP、TAR等格式)应用程序的安全漏洞。攻击者通过精心构造的压缩文件,诱导目标应用程序在解压时将文件提取到预期目录之外的位置,可能导致: 覆盖重要系统文件 执行恶意代码 破坏系统正常运行 2. 攻击原理详解 攻击核心在于 路径遍历序列 的利用: 攻击者构造包含特殊路径的压缩文件(如使用"../"表示上级目录) 应用程序解压时未对路径进行严格验证 文件被解压到预期目录之外的位置 典型攻击场景 : Web应用允许用户上传并解压文件到"/uploads"目录 攻击者上传包含路径为"../../etc/passwd"的压缩文件 应用解压时覆盖系统"/etc/passwd"文件 3. 各语言不安全实现分析 3.1 Java 不安全实现 3.1.1 ZipInputStream 漏洞点 : 直接使用 entry.getName() 构建路径 未对路径进行清理或验证 3.1.2 ZipFile 漏洞点 : 直接使用 entry.getName() 构建目标路径 未检查文件名中的"../"等遍历字符 3.1.3 TarArchiveInputStream 漏洞点 : 直接使用 entry.getName() 解析路径 未对路径进行规范化处理 3.2 Python 不安全实现 3.2.1 ZipFile + shutil.copyfileobj() 漏洞点 : os.path.join 未规范化路径 允许文件名中包含"../"引用父目录 3.2.2 TarFile.extract() 漏洞点 : extract() 方法不会自动移除多余的分隔符和点 直接使用压缩包中的路径信息 3.3 Golang 不安全实现 3.3.1 archive/zip 漏洞点 : 直接使用 f.Name 构建路径 缺少路径安全验证 3.3.2 archive/tar 漏洞点 : 直接使用 header.Name 构建目标路径 缺少路径安全验证 3.4 Ruby 不安全实现 3.4.1 zip模块 漏洞点 : extract() 方法不会自动处理多余的分隔符和点 直接使用 entry.name 构建路径 3.4.2 TarReader模块 漏洞点 : 直接使用 entry.full_name 构建路径 缺少路径规范化处理 4. 构造恶意压缩文件的方法 4.1 ZIP类型 4.2 TAR类型 4.3 TAR.GZ类型 5. 安全防御措施 5.1 通用防御原则 路径规范化 :使用规范化函数处理所有路径 路径验证 :检查解压路径是否在目标目录内 白名单验证 :只允许特定文件类型或名称模式 权限最小化 :使用最低必要权限运行解压操作 5.2 各语言安全实现示例 5.2.1 Java安全实现 5.2.2 Python安全实现 5.2.3 Golang安全实现 5.2.4 Ruby安全实现 6. 最佳实践总结 始终验证解压路径 :确保解压路径在目标目录内 使用绝对路径 :避免相对路径带来的不确定性 规范化所有路径 :使用语言提供的规范化函数 限制文件权限 :使用最低必要权限运行解压操作 考虑使用安全库 :如Python的 zipfile 模块的安全方法 输入验证 :对上传的压缩文件进行严格验证 日志记录 :记录解压操作以便审计 沙箱环境 :在高风险场景下考虑在沙箱中执行解压 7. 测试与验证 实施防御措施后,应进行以下测试: 路径遍历测试 :尝试上传包含"../"的压缩文件 符号链接测试 :测试对符号链接的处理 绝对路径测试 :测试包含绝对路径的压缩文件 嵌套路径测试 :测试深度嵌套的路径 性能测试 :确保安全措施不影响正常功能 通过全面理解和实施这些安全措施,可以有效防御ZIP Slip攻击,保护应用程序免受目录穿越漏洞的威胁。