java 文件安全
java学习笔记
sun.misc.Unsafe
sun.misc.Unsafe是Java底层API(仅限Java内部使用,反射可调用)提供的一个神奇的Java类,Unsafe提供了非常底层的内存、CAS、线程调度、类、对象等操作。
参考
1 | https://zhuanlan.zhihu.com/p/348052930 |
1 | https://www.javasec.org/javase/Unsafe/ |
如何获取Unsafe对象
在使用Unsafe之前,我们需要创建Unsafe对象的实例。这并不像Unsafe unsafe = new Unsafe()这么简单,因为Unsafe的构造器是私有的。它也有一个静态的getUnsafe()方法,但如果你直接调用Unsafe.getUnsafe(),你可能会得到SecurityException异常。只能从受信任的代码中使用这个方法。
sun.misc.Unsafe代码片段:
1 | import sun.reflect.CallerSensitive; |
简单解析一下代码:
前面的代码都是private,后面@CallerSensitive:这是一个注解,表示该方法与调用它的类相关。具体来说,CallerSensitive 注解用于标记那些可能依赖于调用方法的类(或者类加载器)信息的方法。这个注解是 sun.reflect 包中的一个特殊注解,主要用于反射相关的操作。
Reflection.getCallerClass():这个方法通过反射获取调用 getUnsafe() 方法的类。它返回的 Class 对象代表了调用 getUnsafe() 方法的类的类型。
if (var0.getClassLoader() != null):检查调用该方法的类是否由类加载器加载。如果 var0.getClassLoader() 不为 null,则表示调用 getUnsafe() 方法的类是由类加载器加载的,而不是由引导类加载器(bootstrap class loader)加载的。
throw new SecurityException(“Unsafe”):如果调用 getUnsafe() 方法的类有自己的类加载器(getClassLoader() != null),则抛出 SecurityException。这是为了避免 Unsafe 类被不安全的代码访问。这个检查是为了确保只有受信任的代码能够访问 Unsafe。
else { return theUnsafe; }:如果调用者是由引导类加载器加载的(即安全的调用者),则返回 theUnsafe,即 Unsafe 类的实例。
更清晰的解读
1 | @CallerSensitive |
由上代码片段可以看到,Unsafe类是一个不能被继承的类且不能直接通过new的方式创建Unsafe类实例,如果通过getUnsafe方法获取Unsafe实例还会检查类加载器,默认只允许Bootstrap Classloader调用。
既然无法直接通过Unsafe.getUnsafe()的方式调用,那么可以使用反射的方式去获取Unsafe类实例
调用方式一:
因为该类将他的实例化定义在theUnsafe成员变量里面,所以可以使用反射直接获取该变量的值。
ps:Class aClass 是一种泛型语法,用来声明一个 Class 类型的变量,而其中的 表示 通配符(Wildcard),它用于表示未知类型。
调用方式二:
还有种方式就是反射调用getUnsafe()方法,该方法会直接返回UNsafe实例对象。那么可以反射获取该构造方法的实例,然后调用该方法
allocateInstance无视构造方法创建类实例
如果因为某种原因我们不能直接通过反射的方式去创建UnSafeTest类实例,那么这个时候使用Unsafe的allocateInstance方法就可以绕过这个限制了。
UnSafeTest test = (UnSafeTest) unsafe1.allocateInstance(UnSafeTest.class);
Google的GSON库在JSON反序列化的时候就使用这个方式来创建类实例,在渗透测试中也会经常遇到这样的限制,比如RASP限制了java.io.FileInputStream类的构造方法导致我们无法读文件或者限制了UNIXProcess/ProcessImpl类的构造方法导致我们无法执行本地命令等。
java文件系统
在Java SE中内置了两类文件系统:java.io和java.nio,java.nio的实现是sun.nio,文件系统底层的API实现如下图:
简单解析一下这个图片:java.io 包:
java.io.FileSystem:表示一个文件系统的抽象,通常用于进行文件和目录的操作。
java.io.FileInputStream 和 java.io.FileOutputStream:这两个类用于文件的输入输出操作,分别用于从文件中读取数据和向文件写入数据。
java.io.RandomAccessFile:这个类允许随机访问文件内容,可以在文件的任意位置读取和写入。
sun.nio 包:
sun.nio.ch.FileChannelImpl:表示 NIO(New I/O)中的文件通道实现类,允许高效地读写文件。
sun.nio.ch.FileDispatcher:一个文件调度器,负责文件操作的具体实现,可能是通过本地方法与操作系统交互。
sun.nio.ch.NativeDispatcher:负责与操作系统的底层文件操作相关的调度和实现。
sun.nio.ch.SocketDispatcher:负责处理与套接字(Socket)相关的操作。
sun.nio.ch.DatagramDispatcher:负责处理数据报(Datagram)相关的操作。
sun.nio.fs 子包:实现操作系统特定的文件系统和文件操作:
WindowsNativeDispatcher:针对 Windows 系统的本地文件操作实现。
UnixNativeDispatcher:针对 Unix 系统的本地文件操作实现。
UnixCopyFile:可能用于在 Unix 系统中复制文件。
操作系统特定的类:
java.io.WinNTFileSystem 和 java.io.UnixFileSystem:分别处理 Windows 和 Unix 系统上的文件系统差异,提供不同的文件系统实现。
都是一些定义类的,不多了解
Java IO 文件系统
Java抽象出了一个叫做文件系统的对象:java.io.FileSystem,不同的操作系统有不一样的文件系统,例如Windows和Unix就是两种不一样的文件系统: java.io.UnixFileSystem、java.io.WinNTFileSystem
java.io.FileSystem是一个抽象类,它抽象了对文件的操作,不同操作系统版本的JDK会实现其抽象的方法从而也就实现了跨平台的文件的访问操作。
但是Java只不过是实现了对文件操作的封装而已,最终读写文件的实现都是通过调用native方法实现的。
不过需要特别注意一下几点:
1 | 并不是所有的文件操作都在java.io.FileSystem中定义, |
简单的解读:
- Java 文件操作的封装与底层调用
Java 通过 java.io.FileInputStream、java.io.FileOutputStream 和 java.io.RandomAccessFile 等类提供文件读写的 API。
然而,这些文件操作的实现实际上是通过 本地方法(native methods)与操作系统进行交互的。
例如:java.io.FileInputStream#read0 和 java.io.RandomAccessFile#read0 方法内部是通过本地代码实现文件读取。
这些本地方法通常是通过 JNI(Java Native Interface)或直接调用操作系统的底层 API 来执行文件的读取和写入操作。 - Java 中的两类文件系统 API
Java 中的文件操作 API 可以分为两类:
基于阻塞模式的 I/O 文件系统(传统的 I/O):
这类文件操作是基于传统的阻塞 I/O 模型,Java 通过 java.io 包中的类(如 FileInputStream、FileOutputStream)来提供文件读写操作。
在传统的阻塞 I/O 中,当一个线程正在执行 I/O 操作时(例如读取文件),该线程会被阻塞,直到 I/O 操作完成才会继续执行。
阻塞模式 I/O:例如,FileInputStream.read() 和 FileOutputStream.write() 操作会阻塞调用线程,直到数据读取完成或写入完成。
基于 NIO.2 的文件系统(Java 7+):
从 Java 7 开始,Java 引入了 NIO.2(New I/O 2),其主要特点是非阻塞 I/O 和文件系统操作的改进。NIO.2 通过 java.nio.file 包提供了更高效的文件 I/O 操作。
NIO.2 支持 非阻塞 I/O,即在执行文件操作时,线程不必等待操作完成,可以继续执行其他任务。这是通过引入 通道(Channel) 和 选择器(Selector) 等机制来实现的。
NIO.2 提供了更强大的功能,比如文件路径操作(Path)、文件访问权限操作、异步文件 I/O 等。
3. 传统阻塞 I/O vs NIO.2
传统阻塞 I/O:对于 FileInputStream 和 FileOutputStream,这些类的 I/O 操作是基于阻塞模式的,即线程在执行文件读写时会被阻塞,直到操作完成。这种方式简单,但在处理大量 I/O 操作时效率较低,因为线程可能会在等待 I/O 完成时处于闲置状态。
NIO.2(非阻塞 I/O):Java 7+ 的 NIO.2 提供了更加高效的文件操作 API,并引入了 java.nio.file.Files 和 java.nio.file.Path 等类,可以更加灵活地操作文件、目录等资源。NIO.2 的优势是它支持非阻塞 I/O,这意味着应用程序可以在执行文件操作时,继续处理其他任务,而不必等待文件操作完成。
NIO的文件操作在不同的系统的最终实现类也是不一样的,比如Mac的实现类是: sun.nio.fs.UnixNativeDispatcher,而Windows的实现类是sun.nio.fs.WindowsNativeDispatcher。
合理的利用NIO文件系统这一特性我们可以绕过某些只是防御了java.io.FileSystem的WAF/RASP。
Java IO/NIO多种读写文件方式
java.io.FileInputStream类提供了对文件的读取功能,Java的其他读取文件的方法基本上都是封装了java.io.FileInputStream类,比如:java.io.FileReader
使用FileInputStream实现文件读取Demo:
成功读取到文件
调用链如下:
1 | java.io.FileInputStream.readBytes(FileInputStream.java:219) |
写文件Demo:
代码逻辑比较简单: 打开文件->写内容->关闭文件,调用链和底层实现分析请参考FileInputStream
Java 文件名空字节截断漏洞
空字节截断漏洞在很多语言都存在,究其根本是java在调用文件系统(C实现)读写文件是导致的漏洞,并不是java本身的安全问题,高版本的jdk已经处理了这个问题。
修复前(Java SE 7 Update 25)和修复后(Java SE 7 Update 40)的对比会发现Java SE 7 Update 25中的java.io.File类中并未添加\u0000的检测。
漏洞利用场景
截断成功