Java文件操作类漏洞总结

文件操作相关漏洞包括文件包含、文件上传、文件读取、文件写入、文件删除、文件解压漏洞。

1、Java文件操作关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
File
FileUpload
MultipartFile
FileInputStream
FileOutputStream
MultipartRequestEntity
FileUtils
UploadHandleServlet
FileLoadServlet
createNewFile
FileReader
RandomAccessFile
ImageIO
DiskFileItemFactory
ZipInputStream
ZipEntry
...

2、文件包含

参考链接:https://www.cnblogs.com/jinqi520/p/9360713.html

静态包含:

1
<%@include file="top.jsp"%>

动态包含:

1
<jsp:include page="<%=file%>" />

漏洞实例:tomcat AJP cve-2020-1938分析

3、模拟任意文件读取/文件上传

模拟客户端上传文件到服务器 或者从服务器下载文件:

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
import org.apache.commons.io.FilenameUtils;

import java.io.*;
/*
* 模拟客户端上传、下载文件
* */
public class FileUploadOrDownload {
public static void main(String[] args) throws IOException {
// String fileName = "111.png";
String fileName = "../../../../../../../../../../../../../../../../111.png";
FileUploadOrDownload fileUploadOrDownload = new FileUploadOrDownload();
fileUploadOrDownload.getImg(fileName);
}

private void getImg(String fileName){
try {
// 1、模拟客户端上传文件
// 2、或者服务器端返回文件给客户端
// FileInputStream imgFile = this.getImgFileNoFix(fileName);
FileInputStream imgFile = this.getImgFileWithFix(fileName);
if(imgFile==null){
System.out.println("File not found!");
return;
}

// 1、模仿客户端下载,设置文件保存的路径和文件名 跟上面注释1对应
// 2、或者服务器接收客户端上传的文件,设置文件保存的路径和文件名 跟上面注释2对应
File tmp = new File("./owasp/src/main/resources/tmp", "tmp");
FileOutputStream fileOutputStream = new FileOutputStream(tmp);
int len;
byte[] bytes = new byte[1024];
while ((len=imgFile.read(bytes))!=-1){
fileOutputStream.write(bytes,0,len);
}
fileOutputStream.close();
imgFile.close();
}catch (Exception e){
e.printStackTrace();
}

}

private FileInputStream getImgFileWithFix(String fileName) {
// 获取文件名,不包含路径(过滤路径穿越相关符号)
fileName = FilenameUtils.getName(fileName);
// 加白名单判断后缀
String[] whiteList = {"png","jpg","jpeg","gif"};
if(!FilenameUtils.isExtension(fileName.toLowerCase(), whiteList)){
System.out.println("File extension is not allow!");
return null;
}
File file = new File("./owasp/src/main/resources/imgs", fileName);
if(!file.exists()){
System.out.println("File not found!");
return null;
}
try {
return new FileInputStream(file);
} catch (FileNotFoundException e) {
return null;
}
}

private FileInputStream getImgFileNoFix(String fileName) {
File file = new File("./owasp/src/main/resources/imgs", fileName);
if(!file.exists()){
System.out.println("File not found!");
return null;
}
try {
return new FileInputStream(file);
} catch (FileNotFoundException e) {
return null;
}
}

}

如果只从客户端/浏览器端禁止上传的文件类型,而后端没有对文件做限制(如上面的getImgFileNoFix方法),那么就可以绕过前端,抓包修改后缀,然后上传文件,另外就是注意校验请求的MimeType时一定也要校验文件后缀。

3、文件删除:

文件删除 个上面类似的修复原理,避免路径穿越和指定文件类型白名单。

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
import java.io.File;

public class FileDelete {
public static void main(String[] args) {
String fileName = "../../../../../../../../../../../../../../../../111.png";
FileDelete fileDelete = new FileDelete();
// fileDelete.fileDeleteNoFix(fileName);
fileDelete.fileDeleteWithFix(fileName);
}

private Boolean fileDeleteNoFix(String fileName) {
File file = new File("./owasp/src/main/resources/imgs", fileName);
if (file.exists() && file.delete()) {
System.out.println("Delete success!");
return true;
}
System.out.println("Delete fail!");
return false;
}

private boolean fileDeleteWithFix(String fileName) {
File file = new File("./owasp/src/main/resources/imgs", fileName);
// 获取文件名,不包含路径(过滤路径穿越相关符号)
int index = fileName.lastIndexOf(".");
//如果使用 int index = fileName.indexOf("."); ,会获取第一个.的索引,可以用1.png.jsp绕过,修复失败
String extension = fileName.substring(index);
// 加白名单判断后缀
String[] whiteList = {".png", ".jpg", ".jpeg", ".gif"};
for (String list : whiteList) {
if (extension.toLowerCase().equals(list)) {
if (file.exists() && file.delete()) {
System.out.println("Delete success!");
return true;
}
System.out.println("Delete fail!");
return false;
}
}
System.out.println("File extension is not allow!");
return false;

}

}

4、文件解压

解压文件时需要小心谨慎,有两个特别的问题需要避免:一是提取出的文件标准路径落在解压的目标目录之外,一是提取出的文件消耗过多的系统资源。

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
88
89
90
91
92
93
94
95
96
public class FileUnzip {
public static void main(String[] args) {
try{
FileUnzip fileUnzip = new FileUnzip();
fileUnzip.unzipNoFix("./owasp/src/main/resources/1.zip");
}catch (Exception e){
e.printStackTrace();
}
}

private static final int BUFFER = 512;
private static final int TOOBIG = 0x6400000; // 100MB
private static final int TOOMANY = 1024; // max number of files


public final void unzipNoFix(String filename) throws java.io.IOException {
FileInputStream fis = new FileInputStream(filename);
ZipInputStream zis = new ZipInputStream(new BufferedInputStream(fis));
ZipEntry entry;
try {
while ((entry = zis.getNextEntry()) != null) {
System.out.println("Extracting: " + entry);
int count;
byte[] data = new byte[BUFFER];
// Write the files to the disk, but only if the file is not insanely big
if (entry.getSize() > TOOBIG) {
throw new IllegalStateException("File to be unzipped is huge.");
}
if (entry.getSize() == -1) {
throw new IllegalStateException("File to be unzipped might be huge.");
}
FileOutputStream fos = new FileOutputStream(entry.getName());
BufferedOutputStream dest = new BufferedOutputStream(fos, BUFFER);
while ((count = zis.read(data, 0, BUFFER)) != -1) {
dest.write(data, 0, count);
}
dest.flush();
dest.close();
zis.closeEntry();
}
} finally {
zis.close();
}
}


public final void unzipWithFix(String fileName) throws java.io.IOException {
FileInputStream fis = new FileInputStream(fileName);
ZipInputStream zis = new ZipInputStream(new BufferedInputStream(fis));
ZipEntry entry;
int entries = 0;
int total = 0;
byte[] data = new byte[BUFFER];
try {
while ((entry = zis.getNextEntry()) != null) {
System.out.println("Extracting: " + entry);
int count;
// Write the files to the disk, but ensure that the entryName is valid,
// and that the file is not insanely big
String name = sanitzeFileName(entry.getName(), ".");
FileOutputStream fos = new FileOutputStream(name);
BufferedOutputStream dest = new BufferedOutputStream(fos, BUFFER);
while (total + BUFFER <= TOOBIG && (count = zis.read(data, 0, BUFFER)) != -1) {
dest.write(data, 0, count);
total += count;
}
dest.flush();
dest.close();
zis.closeEntry();
entries++;
if (entries > TOOMANY) {
throw new IllegalStateException("Too many files to unzip.");
}
if (total > TOOBIG) {
throw new IllegalStateException(
"File being unzipped is too big.");
}
}
} finally {
zis.close();
}
}

// 将文件放在指定目录,防止 ../ 路径穿越
private String sanitzeFileName(String entryName, String intendedDir) throws IOException {
File f = new File(intendedDir, entryName);
String canonicalPath = f.getCanonicalPath();
File iD = new File(intendedDir);
String canonicalID = iD.getCanonicalPath();
if (canonicalPath.startsWith(canonicalID)) {
return canonicalPath;
} else {
throw new IllegalStateException("File is outside extraction target directory.");
}
}
}

5、总结:

做Java 文件类代码审计时,尤其需要注意文件后缀和文件的路径是否规范,是否有存在绕过的可能。

文件上传一般修复思路:

  1. 对于上传文件的后缀名截取校验时,忽略大小写,采用统一大写或小写方式进行校验;
  2. 严格检测上传文件的类型,推荐白名单校验;
  3. jdk版本小于7u40可能存在截断漏洞,注意jdk版本;
  4. 限制文件上传的大小和频率;
  5. 文件类型校验(getContentType)
  6. 可对上传文件进行重命名、自定义后缀。

6、参考链接:

1
2
3
https://www.cnblogs.com/nice0e3/p/13698256.html
https://blog.m1kh.com/index.php/archives/739/
https://www.cnblogs.com/jinqi520/p/9360713.html