[toc]

Java是一个跨平台的语言,不同的操作系统有着完全不一样的文件系统和特性。JDK会根据不同的操作系统(AIX,Linux,MacOSX,Solaris,Unix,Windows)编译成不同的版本。

在Java语言中对文件的任何操作最终都是通过JNI调用C语言函数实现的。Java为了能够实现跨操作系统对文件进行操作抽象了一个叫做FileSystem的对象出来,不同的操作系统只需要实现起抽象出来的文件操作方法即可实现跨平台的文件操作了。

在Java SE中内置了两类文件系统:java.io和java.nio,java.nio的实现是sun.nio,文件系统底层的API实现如下图:

Java FileSystem - 图1
https://javasec.org/javase/FileSystem/FileSystem.html

Java IO/NIO多种读写文件方式

Java 对文件的读写分为了基于阻塞模式的IO和非阻塞模式的NIO
常读写文件都是使用的阻塞模式,与之对应的也就是java.io.FileSystem。java.io.FileInputStream类提供了对文件的读取功能,Java的其他读取文件的方法基本上都是封装了java.io.FileInputStream类,比如:java.io.FileReader。

FileInputStream

  1. package m0nk3y.learn.java.classloader;
  2. import java.io.*;
  3. public class FileInputtStreamDemo {
  4. public static void main(String[] args) throws IOException {
  5. File file = new File("/etc/passwd");
  6. // 打开文件对象并创建文件输入流
  7. FileInputStream fis = new FileInputStream(file);
  8. // 定义每次输入流读取到的字节数
  9. int a = 0;
  10. // 定义缓冲区大小
  11. byte[] bytes = new byte[1024];
  12. // 创建二进制输出对象
  13. ByteArrayOutputStream out = new ByteArrayOutputStream();
  14. // 循环读取内容
  15. while ((a = fis.read(bytes)) != -1) {
  16. out.write(bytes, 0, a); // 从bytes 数组下标的0开始截取,a表示输入流read读取到的字节数
  17. }
  18. System.out.println(out.toString());
  19. }
  20. }

Java FileSystem - 图2

FileOutoutStream

RandomAccessFile

任意文件读取特性体现在如下方法:

// 获取文件描述符
public final FileDescriptor getFD() throws IOException

// 获取文件指针
public native long getFilePointer() throws IOException;

// 设置文件偏移量
private native void seek0(long pos) throws IOException;
java.io.RandomAccessFile类中提供了几十个readXXX方法用以读取文件系统,最终都会调用到read0或者readBytes方法,我们只需要掌握如何利用RandomAccessFile读/写文件就行了。

FileSystemProvider

JDK7新增的NIO.2的java.nio.file.spi.FileSystemProvider,利用FileSystemProvider我们可以利用支持异步的通道(Channel)模式读取文件内容

基于NIO的文件读取逻辑是:打开FileChannel->读取Channel内容。

打开FileChannel的调用链为:

sun.nio.ch.FileChannelImpl.(FileChannelImpl.java:89)
sun.nio.ch.FileChannelImpl.open(FileChannelImpl.java:105)
sun.nio.fs.UnixChannelFactory.newFileChannel(UnixChannelFactory.java:137)
sun.nio.fs.UnixChannelFactory.newFileChannel(UnixChannelFactory.java:148)
sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:212)
java.nio.file.Files.newByteChannel(Files.java:361)
java.nio.file.Files.newByteChannel(Files.java:407)
java.nio.file.Files.readAllBytes(Files.java:3152)
com.anbai.sec.filesystem.FilesDemo.main(FilesDemo.java:23)
文件读取的调用链为:

sun.nio.ch.FileChannelImpl.read(FileChannelImpl.java:147)
sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:65)
sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:109)
sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:103)
java.nio.file.Files.read(Files.java:3105)
java.nio.file.Files.readAllBytes(Files.java:3158)
com.anbai.sec.filesystem.FilesDemo.main(FilesDemo.java:23)

Java 文件名空字节截断漏洞

空字节截断漏洞漏洞在诸多编程语言中都存在,究其根本是Java在调用文件系统(C实现)读写文件时导致的漏洞,并不是Java本身的安全问题。不过好在高版本的JDK在处理文件时已经把空字节文件名进行了安全检测处理。

Java 空子节文件名漏洞历史

2013年9月10日发布的Java SE 7 Update 40修复了空字节截断这个历史遗留问题。此次更新在java.io.File类中添加了一个isInvalid方法,专门检测文件名中是否包含了空字节。
Java FileSystem - 图3

修复的JDK版本所有跟文件名相关的操作都调用了isInvalid方法检测,防止文件名空字节截断。

受空字节截断影响的JDK版本范围:JDK<1.7.40,单是JDK7于2011年07月28日发布至2013年09月10日发表Java SE 7 Update 40这两年多期间受影响的就有16个版本,值得注意的是JDK1.6虽然JDK7修复之后发布了数十个版本,但是并没有任何一个版本修复过这个问题,而JDK8发布时间在JDK7修复以后所以并不受此漏洞影响。

漏洞测试(直接嫖截图)

Java FileSystem - 图4

如果是已经修复的jdk版本,测试写文件截断时抛出java.io.FileNotFoundException: Invalid file path异常:
Java FileSystem - 图5

空字节截断利用场景

Java空字节截断利用场景最常见的利用场景就是文件上传时后端获取文件名后使用了endWith、正则使用如:.(jpg|png|gif)$验证文件名后缀合法性且文件名最终原样保存,同理文件删除(delete)、获取文件路径(getCanonicalPath)、创建文件(createNewFile)、文件重命名(renameTo)等方法也可适用。

空字节截断修复方案

最简单直接的方式就是升级JDK,如果担心升级JDK出现兼容性问题可在文件操作时检测下文件名中是否包含空字节,如JDK的修复方式:fileName.indexOf(‘\u0000’)即可。