High version jdk bypass of jndi injection in Java deserialization

The leaders in the group have fun. Take out the vegetable chicken, sort it out and study it, and fry some cold rice.

It mainly includes the following three parts:

  1. jndi injection principle
  2. jndi injection and deserialization
  3. jndi injection and jdk version

jndi injection principle:

JNDI (Java name and Dictionary interface), a set of Java EE standards, is similar to the Windows registry.

The structure is as follows:

key: route+name
value: Stored data (in jndi Objects are stored in Object)

jndi is java's API for accessing directory and naming services. Using jndi to query is a normal function, but because the security problem is not considered in the implementation, if a malicious object is queried, it will be attacked. However, not all attacks can lead to RCE (such as dnslog2333)
JNDI query is divided into the following two steps:

1. The client requests a naming service and gets the object

2. The client resolves this object

These two steps may lead to vulnerabilities. jndi supports four protocols that may lead to danger: LDAP, RMI, DNS and CORBA, each of which corresponds to different implementation methods. The objects that support binding also include reference objects, deserialization objects, attribute objects, etc., so there are many attack methods and vulnerabilities.

The most common and dangerous attacks are jndi+rmi and jndi+ldap. corba can also be used for command execution (but it was repaired earlier, and rmi can be used for corba)

JNDI+RMI

The key code is located in RegistryContext#lookup

public Object lookup(Name name) throws NamingException {
  if (name.isEmpty()) {
    return (new RegistryContext(this));
  }
  Remote obj;
  try {
    obj = registry.lookup(name.get(0));
    //You can see that the remote object is obtained through rmi's native lookup
  } catch (NotBoundException e) {
    throw (new NameNotFoundException(name.get(0)));
  } catch (RemoteException e) {
    throw (NamingException)wrapRemoteException(e).fillInStackTrace();
  }
  return (decodeObject(obj, name.getPrefix(1)));

You can see that the remote object is obtained through rmi's native lookup, while rmi is obtained through deserialization. At this time, if there is a gadget component in the client system, the deserialization of this step can lead to code execution.
The second step is to parse the obj obtained in the decodeObject. The logic is located in RegistryContext#decodeObject

private Object decodeObject(Remote r, Name name) throws NamingException {
  try {
    Object obj = (r instanceof RemoteReference)
    ? ((RemoteReference)r).getReference(): (Object)r;
    /*
    * Classes may only be loaded from an arbitrary URL codebase when
    * the system property com.sun.jndi.rmi.object.trustURLCodebase
    * has been set to "true".
    */
    //The notes here are very clear
    // Use reference if possible
    Reference ref = null;
    if (obj instanceof Reference) {
    ref = (Reference) obj;
    } else if (obj instanceof Referenceable) {
    ref = ((Referenceable)(obj)).getReference();
    }
    if (ref != null && ref.getFactoryClassLocation() != null &&
    !trustURLCodebase) {
    throw new ConfigurationException(
    "The object factory is untrusted. Set the system property" +
    " 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
  }
  return NamingManager.getObjectInstance(obj, name, this,
  environment);

Notes indicate if com sun. jndi. rmi. object. If trusturlcodebase is true, it can be loaded through codebase
Any remote class that causes code execution. This verification is enabled in jdk8u121 and is added to the RegistryContext, that is, only the rmi implementation of jndi is limited, so the security personnel will discover the use of ldap later.

Object obj = (r instanceof RemoteReference) ? ((RemoteReference)r).getReference(): (Object)r;

This code determines whether the incoming object satisfies the RemoteReference interface. If so, getReference() gets the reference object and enters the getObjectInstance function.

The specific utilization process is as follows:

1. InitialContext. is called in the target code. Lookup (URI), which is controllable by the user;

2. The attacker sets the uri as the malicious rmi service address;

3. The attacker sets rmi server to return a reference object to the target, and a carefully constructed Factory class is specified in the reference object;

4. When the target looks up the remote object, it gets the dynamically loaded and instantiates the Factory class, and then calls Factory Getobjectinstance() loads the external remote object instance;

5. Attackers can write malicious code in the construction method, static code block and getObjectInstance() method of Factory class file to achieve the effect of RCE;

The getInstance() function of this class is used in the getObjectInstance main function to get the object of this class currently instantiated by the system. If the object of this class has not been instantiated by the current system, the constructor of this class will be called.
You can export two subsequent command execution and utilization methods:

if (ref != null) {
            String f = ref.getFactoryClassName();
            if (f != null) {
                // if reference identifies a factory, use exclusively
 
 
 
 
                factory = getObjectFactoryFromReference(ref, f); //Trigger point 1
                if (factory != null) {
                    return factory.getObjectInstance(ref, name, nameCtx,
                                                     environment); //Trigger point 2
                }
                // No factory found, so return original refInfo.
                // Will reach this point if factory class is not in
                // class path and reference does not contain a URL for it
                return refInfo;

The first is getObjectFactoryFromReference(). In this function, the corresponding malicious class object will be obtained. When trustURLCodebase is opened, the remote class can be loaded and instantiated through URLClassloader. Class Newinstance() triggers a malicious constructor:

return (clas != null) ? (ObjectFactory) clas.newInstance() : null;

The second is to call the getObjectInstance function through the instantiated class to execute malicious code:

As long as an attacker implements the ObjectFactory interface and rewrites getObjectInstance, malicious code can be executed.

public class Exec implements ObjectFactory {
    public Exec(){}
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        System.out.println("factory.getObjectInstance hook!");
        return null;
    }
}

To sum up, RMI's RCE utilization, in a sense, does not use code execution caused by deserialization, but only uses deserialization to load malicious remote objects.

JNDI+LDAP

Core logic in ldapCtx#c_lookup

protected Object c_lookup(Name name, Continuation cont)
throws NamingException {
cont.setError(this, name);
Object obj = null;
Attributes attrs;
try {
......
if (attrs.get(Obj.JAVA_ATTRIBUTES[Obj.CLASSNAME]) != null) {
// Serialize object or serialize reference
obj = Obj.decodeObject(attrs);
}
if (obj == null) {
obj = new LdapCtx(this, fullyQualifiedName(name));
}
} catch (LdapReferralException e) {
......
try {
return DirectoryManager.getObjectInstance(obj, name,
this, envprops, attrs);
......
}

Just look at the key code, which is mainly divided into two steps:
First, through obj Deocodeobject obtains the string from ldap, decodes an ldap object, and then uses directorymanager Getobjectinstance parsing is the same logic as RMI, except that there is no RMI verification on trusrURLCodebase.

Therefore, jndi+ldap obtains objects in the same way as rmi. They are obtained through deserialization. Then the object is parsed, and directorymanager. Exe is called Getobjectinstance is actually the same as namingmanager Getobjectinstance is basically the same. Decodereference, native deserialization, but if com sun. jndi. ldap. object. When trusturlcodebase is enabled, an overridden resolveClass will be called for remote class loading.

1. When parsing the object, call getObjectFactoryFromReference and open com sun. jndi. ldap. object. Remote class loading when trusturlcodebase

2. Like rmi, the local factory class is used, but the ldap server cannot bind the remote object directly like rmi. It needs to bind the serialized data.

JNDI injection and jdk version

jdk has twice repaired the use of JNDI injection. 8u121 restricts the JNDI injection of RMI and corba. Com sun. jndi. ldap. object. Trusturlcodebase restricts both services from loading remote factory classes.

8u191 remote class loading for ldap is disabled.

So far, the jndi injection available in the higher version includes: loading the local factory class, opening the local deserialization chain,

The former was introduced by tomcat8/9, and the latter requires a local deserialization chain.

Summary:

1. Principle of jndi injection

The general principle of jndi injection is remote class loading. Other attack methods include local factory class code execution and deserialization.

2. jndi injection and deserialization

jndi injection relies on deserialization to pass objects, but it is often said that the execution of jndi injection code is not caused by the deserialization chain. Similarly, jndi injection can also be transformed into a commonly known deserialization attack.

3. jndi injection and jdk upgrade

jdk upgrade can only repair the attack mode of jndi remote class loading. The high version still has the attack mode of loading local factory classes and deserializing local utilization chains.

Keywords: Java Web Security

Added by xconspirisist on Tue, 11 Jan 2022 13:17:44 +0200