那些默认不安全的ZIP Slip攻击
字数 1643 2025-08-22 12:23:19
ZIP Slip 压缩目录穿越攻击全面解析与防御指南
1. ZIP Slip 攻击概述
ZIP Slip(压缩目录穿越攻击)是一种针对处理压缩文件(如ZIP、TAR等格式)应用程序的安全漏洞。攻击者通过精心构造的压缩文件,诱导目标应用程序在解压时将文件提取到预期目录之外的位置,可能导致:
- 覆盖重要系统文件
- 执行恶意代码
- 破坏系统正常运行
2. 攻击原理详解
攻击核心在于路径遍历序列的利用:
- 攻击者构造包含特殊路径的压缩文件(如使用"../"表示上级目录)
- 应用程序解压时未对路径进行严格验证
- 文件被解压到预期目录之外的位置
典型攻击场景:
- 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 通用防御原则
- 路径规范化:使用规范化函数处理所有路径
- 路径验证:检查解压路径是否在目标目录内
- 白名单验证:只允许特定文件类型或名称模式
- 权限最小化:使用最低必要权限运行解压操作
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. 最佳实践总结
- 始终验证解压路径:确保解压路径在目标目录内
- 使用绝对路径:避免相对路径带来的不确定性
- 规范化所有路径:使用语言提供的规范化函数
- 限制文件权限:使用最低必要权限运行解压操作
- 考虑使用安全库:如Python的
zipfile模块的安全方法 - 输入验证:对上传的压缩文件进行严格验证
- 日志记录:记录解压操作以便审计
- 沙箱环境:在高风险场景下考虑在沙箱中执行解压
7. 测试与验证
实施防御措施后,应进行以下测试:
- 路径遍历测试:尝试上传包含"../"的压缩文件
- 符号链接测试:测试对符号链接的处理
- 绝对路径测试:测试包含绝对路径的压缩文件
- 嵌套路径测试:测试深度嵌套的路径
- 性能测试:确保安全措施不影响正常功能
通过全面理解和实施这些安全措施,可以有效防御ZIP Slip攻击,保护应用程序免受目录穿越漏洞的威胁。