Java代码审计:文件篇/文件上传/文件读取/目录遍历

0x00 前提

自学Java 代码审计,主要自己一个人学习,有点闭门造车,搜索引擎学习法,但是还是记录一下,也分享一下,也便于将来的总结和反思,如果我能终能学到什么,我也会重新梳理思路,为那些自学者提供一个好的思路,所以有了下面的系列文章java代码审计自学篇。

0x01 文件路径穿越

简述:

  • 许多的文件漏洞都是来源于文件路径的问题,好多时候也是路径可控,再加上一下程序员奇怪的逻辑。
  • 如果漏洞路径可控提供很多其他突破的方法
  • 攻击者利用../可以上传至任意指定目录或者目录穿越。

示例代码:

中间有../可以造成文件路径的不安全

1
2
3
4
5
6
7
8
9
10
11
12
13
package file;

import java.io.File;
import java.io.IOException;

public class filepath {
public static void main(String[] args) throws IOException {
File file = new File("../../file/123.txt");
System.out.println(file.getAbsolutePath());
System.out.println(file.getCanonicalPath());
System.out.println(file.exists());
}
}

image-20200809163340930

潜在的目录穿越:

一个文件被打开,然后读取文件内容,这个文件名来自于一个输入的参数。如果没有过滤这个传入的参数,那么本地文件系统中任意文件都会被读取。

1
2
3
4
5
6
文件读取有问题,别在里面拼接
val result = Source.fromFile("public/" + value).getLines().mkString // Weak point

修复:要在外面拼接好
filename = "public/" + FilenameUtils.getName(value)
val result = Source.fromFile(filename).getLines().mkString

0x02文件上传

简述:

文件上传过程中,因为未校验上传文件后缀类型,导致用户可上传jsp和jspx等一些webshell文件。

代码审计时可重点关注对上传文件类型是否有足够安全的校验。

漏洞示例:

没有任何过滤

jdk原始的流操作上传

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
public String FileUpload(MultipartFile file){
String fileName = file.getOriginalFilename();
if (fileName==null) {
return "file is error";
}
//目录拼接
String filePath = "/static/images/uploads/"+fileName;
if (!file.isEmpty()) {
try {
//转化字节流
byte[] bytes = file.getBytes();
//创建file对象 转化为BufferedOutputStream对象
BufferedOutputStream stream =
new BufferedOutputStream(new FileOutputStream(new File(filePath)));
//写入流
stream.write(bytes);
//关闭流
stream.close();
return "OK";
} catch (Exception e) {
return e.getMessage();
}
} else {
return "You failed to upload " + file.getOriginalFilename() + " because the file was empty.";
}
}

框架常用的封装类上传

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
private static String UPLOADED_FOLDER = "/tmp/";

public String FileUpload(@RequestParam("file") MultipartFile file,RedirectAttributes redirectAttributes) {
//检测文件是否存
if (file.isEmpty()) {
// 赋值给uploadStatus.html里的动态参数message
redirectAttributes.addFlashAttribute("message", "Please select a file to upload");
return "redirect:/file/status";
}

try {
// 获取文件,上传
// 获取字节流,放入数字
byte[] bytes = file.getBytes();
//获取文件路径,目录拼接 /TMP/ + Filename
Path path = Paths.get(UPLOADED_FOLDER + file.getOriginalFilename());
//文件写入
Files.write(path, bytes);

//回显路径
redirectAttributes.addFlashAttribute("message",
"You successfully uploaded '" + UPLOADED_FOLDER + file.getOriginalFilename() + "'");

} catch (IOException e) {
redirectAttributes.addFlashAttribute("message", "upload failed");
e.printStackTrace();
return "redirect:/file/status";
}

return "redirect:/file/status";
}

审计函数

java中文件操作的函数特别多,有的是原始的字节字符流

java都是基于流的,还有好多都是后面有封装的,感觉如果不熟就直接 搜索file吧,再检查 过滤条件

  1. JDK原始的java.io.FileInputStream

  2. JDK原始的 BufferedOutputStream

  3. JDK原始的各种OutputStream,流操作都可以

  4. Apache Commons IO提供的org.apache.commons.io.FileUtils类

参考园长文章

修复方案

  • 使用白名单校验上传文件类型、大小限制、MIME类型
  • 白名单fileName.substring(fileName.lastIndexOf(“.”)); 检查后缀名
  • 还有一个BufferedImage类、Image类、Graphics类这些封装好的图片类,直接传进去试试
1
BufferedImage bi = ImageIO.read(file);

0x02文件读取

简述:

Java其实读写是一体的,都是流的输入和输出

这个漏洞主要是要结合第一个,路径穿越的情况

代码:

1
2
3
4
5
6
7
8
9
10
@GetMapping("/path_traversal/")
public String getImage(String filepath) throws IOException {
File f = new File(filepath);
if (f.exists() && !f.isDirectory()) {
//读取文件
byte[] data = Files.readAllBytes(Paths.get(filepath));
return new String(Base64.encodeBase64(data));
} else {
return "File doesn't exist or is not a file.";
}

审计函数

  1. JDK原始的java.io.RandomAccessFile类

  2. JDK原始的inputsteam类

  3. Apache Commons IO提供的org.apache.commons.io.FileUtils类

  4. JDK1.7新增的基于NIO非阻塞异步读取文件的java.nio.channels.AsynchronousFileChannel

  5. JDK1.7新增的基于NIO读取文件的java.nio.file.Files

    常用方法如:Files.readAllBytesFiles.readAllLines

    参考园长文章

修复方案:

过滤目录穿越关键字

0x01 目录遍历

简述:

目录遍历,主要看逻辑吧,能不能回显

有专门file.listFiles()函数可以处理。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package file;

import java.io.File;
import java.io.FileFilter;

public class filepath {
public static void main(String[] args) {
String path = "/Users/zy/Desktop/java_rmi/src/main/java/"; //要遍历的路径
File file = new File(path); //获取其file对象
func(file);
}

private static void func(File file){
File[] fs = file.listFiles();
for(File f:fs){
if(f.isDirectory()) //若是目录,则递归打印该目录下的文件
func(f);
if(f.isFile()) //若是文件,直接打印
System.out.println(f);
}
}
}