Dieser Artikel ist auch auf Deutsch verfügbar
I would never have imagined that one day I would be writing an article on the Java Naming and Directory Interface (JNDI). For a long time I only knew JNDI as a mechanism to get a database connection in an application server like Apache Tomcat. But then in December 2021 the Log4Shell vulnerability hit us, and suddenly everyone was talking about JNDI again. So this is a good opportunity to take a look at what JNDI is actually intended for and what role it plays in the security flaw.
As is so often the case in Java, JNDI is an abstraction of various concrete technologies. In this case the programming interface abstracts the access to name and directory services. The two best-known representatives of this are probably the Domain Name System (DNS) and the Lightweight Directory Access Protocol (LDAP).
So that additional technologies can also be attached via the abstraction of JNDI, JNDI consists of two parts. The application programming interface (API) is the part which we use within an application. The service provider interface (SPI) offers the mentioned possibility to provide further implementations for JNDI.
In this article however we deal exclusively with the API side. After a brief introduction to the general concepts of JNDI, we will look at some examples with DNS and LDAP. We then turn our attention back to Log4Shell and learn which functionality of JNDI was exploited.
Concepts of JNDI
The core idea of name services is to store an object under a name. For example, in a DNS server
we store the IP address of a domain name. In JNDI this is mapped with the two interfaces
javax.naming.Binding
and javax.naming.Name
.
In addition, we need a context (javax.naming.Context
), which represents a bracket across
multiple bindings. A context can also contain further subcontexts should the service be
hierarchically structured.
In addition to this, there is a second package, javax.naming.directory
, which is intended
for directory services. Directory services extend naming services with attributes
(javax.naming.directory.Attribute
) that can be bound to objects. However, to work with them
we need a special context (javax.naming.directory.DirContext
). This context allows us to
also perform a search for objects with specific attribute values.
DNS
One of the default JNDI implementations in the JDK is DNS. DNS is primarily used to store a mapping of domain names to IP addresses, so-called A records. However, in addition to these, there are other possible records that are managed using DNS. For example, MX records point to the mail servers responsible for a domain, and TXT records let us make arbitrary entries for a domain. These are used, for example, by Let’s Encrypt to prove that a domain really belongs to me.
The JNDI implementation attaches the individual records as attributes to an object. For a DNS
query (see Listing 1) we therefore use DirContext
in combination with the getAttributes
method.
The age of the API becomes apparent in two places in particular. First, we have to use a
Hashtable
for configuration when creating InitialDirContext
. Second, the getAll
method
returns us a NamingEnumeration<? extends Attribute>
. So to iterate over all attributes we
can’t use a for-each loop, and creating a stream
is also not directly possible, instead
requiring a while loop in combination with hasMore
and next
.
Apart from that, the code outputs what we would expect. We get a listing of the A, TXT, and MX records of the domain mvitz.de (see Listing 2).
LDAP
In addition to DNS, an implementation for LDAP queries is also part of JNDI. LDAP is mainly known for managing users and their passwords, along with other properties. Probably the most well-known representative for this is ActiveDirectory from Microsoft.
However, LDAP is actually a generic database including a query language in which we can store and query objects and attributes in a tree. LDAP, like SQL databases, relies on schemas that describe objects and their attributes. So we cannot store whatever we want there, but need an exact description of the structure in advance.
For the following examples we use OpenLDAP, which we start via Docker (see Listing 3).
Afterwards, the LDAP server is locally accessible on localhost at ports 389 and 689 and we
can query it by means of JNDI. To do this, we create analogous to the previous DNS query an
InitialDirContext
with a suitable environment, as shown in Listing 4.
Using listBindings
we get, again analogous to getAttributes
from the DNS example, a
NamingEnumeration
. This time, however, it contains Bindings
. We now use the name of
these bindings to fetch the attributes for the entry by means of getAttributes
and
then output the attribute cn In this case, instead of listBindings
, we could have
also used list
, since we are not using the returned object, but only using the name to
retrieve the attributes.
In addition to the listing, we can also use JNDI to perform a search, as seen in Listing 5. In this case, we search below ou=People for all objects that have the ou attribute set to Jedi. The result of the search should also contain the cn attribute in addition to the name, which we then output. The JNDI API of course also provides us with methods to modify objects and attributes. In Listing 6 for example we change the attribute cn of the object with the name uid=leia,ou=People.
Since LDAP is probably the most prominent representative for JNDI, we could have used the
interfaces from the dedicated package javax.naming.ldap
for these queries. These map
additional special LDAP concepts that cannot be used by means of the more generic JNDI API.
However, these are not needed for the examples shown here.
Loading Code from LDAP
So far we have seen what DNS and LDAP queries and modifications look like with JNDI. But which aspect of this is part of such a devastating vulnerability?
So far, although we have used listBindings
to retrieve bindings from the server that link
a name with an object, we have not used the object. And it is exactly this usage that is the
problem. Because JNDI offers us the possibility to query serialized Java objects and also to
use them afterwards.
To do so we store some objects in the LDAP server (see Listing 7). By specifying objectclass, the LDAP server knows which schema the objects follow and which attributes are possible. In addition, we set the two attributes javaclassname and javaSerializedData.
As already seen, we can now query these entries with JNDI (see Listing 8). However, as soon as
we now use the bound object, the serialized object state stored in LDAP is loaded. The direct
lookup
is only used here to be able to cast the object directly. We could have alternatively
gained access to the object during iteration by means of all bindings with getObject
. The
output (see Listing 9) shows that we can store and retrieve objects from the JDK itself
(HashMap
) as well as our own classes (Person
), as long as this class implements the
java.io.Serializable
interface.
In this way it is possible to reload code within an application. But of course, it not only allows us to do this, but potentially attackers as well. So far, this possibility is limited to classes that our application knows. So the attacker would have to find a combination of existing classes that are exploitable by further vulnerabilities.
However, JNDI also allows us to dynamically reload classes unknown to the application. To do so
we set in the LDAP the attribute javaCodebase in addition to the two attributes
javaclassname and javaSerializedData. For the entry in Listing 10, we set this value to a
URL where a JAR file is accessible via HTTP. JNDI now dynamically loads not only the object but
also the previously unknown class en.mvitz.jndi.remote.Hack
during the lookup (see Listing
11). The two calls to System.out.println
with the loaded object subsequently execute the
toString
method on the instance. In this way it is possible to reload arbitrary code as long
as the application can reach the location specified in javaCodebase.
However, this dynamic reloading of classes with JNDI fortunately no longer automatically
works in newer versions of the JDK. If we need this functionality, we have to explicitly
set the Java system property com.sun.jndi.ldap.object.trustURLCodebase
to true
when
starting the application.
Log4Shell
How is JNDI related to Log4Shell? As with many security vulnerabilities, it is a combination of several factors that, when combined, become a problem.
Using the syntax ${...}
, Log4j allows parameters to be included in log messages. The
parameters can be loaded from different sources. One possibility is to
execute a JNDI lookup using ${jndi:...}
.
If the attacker now manages to get us to log such a string, for example by sending a matching HTTP header or query parameter, it is possible, in combination with JNDI, to reload code, as shown above. If dynamic class reloading is enabled, arbitrary code can now be executed. If it is disabled, as is the case by default, an attempt can still be made to exploit classes already present in the application. Schematically, the calls then look as shown in Listing 12 and lead to the output shown in Listing 13.
We can see here that the first lookup was successfully executed and as output we get the value
of the toString
method of our instance from the LDAP.
If the deserialization from the first log statement does not allow an attack, we can still try to retrieve values in conjunction with a lookup for environment variables. Listing 14 shows the log of the LDAP server for the second log statement, where we can read the concrete value of the environment variable HOME from the server.
To protect ourselves against this specific vulnerability, Log4j must be updated to at least version 2.17.1. It is also advisable to use an up-to-date JDK. In addition, it may be useful to restrict outgoing network traffic and thus only allow connections to known destinations.
Developers who want to go even further can also use Java Security Manager to further restrict which code is allowed to call the JNDI API. However, the Security Manager was deprecated in JDK 17 with JEP 411 and marked for removal. It is therefore questionable whether this option will be available for much longer.