Vulnerability Spotlight: CVE-2023-0264
Details for an user impersonation vulnerability within Keycloak
- Name
- Timo Müller
Keycloak is an open source identity and access management solution […] [to] secure services with minimum effort
Due to it’s relative simplicity (at least for a IAM product) and its open source nature Keycloak is a popular authorization server for many of our customers’ projects. As we do our best to stay up to date on emerging vulnerabilities and give our customers a heads up on what to focus on, CVE-2023-0264 with a CVSS rating of 8.3 caught our attention.
In this blogpost we will spotlight a vulnerability which, under certain conditions, allows the impersonation of other Keycloak users. As this vulnerability did not receive a lot of attention we want to showcase how it works, and the potential impact to unpatched systems.
CVE-2023-0264 Management Summary
The CVE-2023-0264 description is kept quite vague. It states that “users can be impersonated” if some kind of “UUID” is known to the attacker. Furthermore, the CVSS rating reveals that some kind of user is required for successfully exploitation. As a first step lets make the CVE description more concise:
A validation error within the login flow of Keycloak allows any existing user to impersonate any other existing user within the same Keycloak realm. In the worst case an attacker with a valid user account can abuse this vulnerability to elevate their privileges to an administrative account. As a condition for this attack the attacker requires the client session ID of a target account.
Any Keycloak instance below version 21.0.1
is affected and if you haven’t done so we advise to patch the instance immediately.
Patch analysis
Even though the CVE description is quite vague, the GitHub Advisory already links us to the relevant commit. Luckily, the commit only contains bug fix changes which makes the exploit discovery quick.
First let’s check the OAuth code generation. After the patch the OAuth code constructor includes an additional parameter userSessionid
.
1public OAuth2Code(
2 String id,
3 int expiration,
4 String nonce,
5 String scope,
6 String redirectUriParam,
7 String codeChallenge,
8 String codeChallengeMethod,
9 String userSessionId
10 )
To support a user session we require Cookies, or any other state-holding identifier. This already rules out all OAuth flows which are not session-bound, such as the Device Authorization Grant (RFC 8628).
Looking further, the newly implemented patch test case tells us everything we need to know…
The test case (see below) performs a “standard” OAuth login with two different test users (Line 4, 10). The login flow retrieves two code(s)
(Line 5, 11) which are used to retrieve the access token for the respective users (Line 20).
A code
contains three parts, joined by a dot (.
). Within lines 12 to 15 the test case takes the middle part of the first code
, and overwrites the middle part of the second code.
Afterwards it attempts to request an access token with this (seemingly) malformed code
(Line 20).
1public void failIfUsingCodeFromADifferentSession() throws IOException {
2 // first client user login
3 oauth.openLoginForm();
4 oauth.doLogin("test-user@localhost", "password");
5 String firstCode = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
6
7 // second client user login
8 OAuthClient oauth2 = new OAuthClient();
9 oauth2.init(driver2);
10 oauth2.doLogin("john-doh@localhost", "password");
11 String secondCode = oauth2.getCurrentQuery().get(OAuth2Constants.CODE);
12 String[] firstCodeParts = firstCode.split("\\.");
13 String[] secondCodeParts = secondCode.split("\\.");
14 secondCodeParts[1] = firstCodeParts[1];
15 secondCode = String.join(".", secondCodeParts);
16
17 OAuthClient.AccessTokenResponse tokenResponse;
18
19 try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithOtherKeyStoreAndTrustStore()) {
20 tokenResponse = oauth2.doAccessTokenRequest(secondCode, "password", client);
21 }
22
23 assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), tokenResponse.getStatusCode());
24}
From the previous test case we can already assume how a successful attack plays out:
- Get the middle
code
part of another user which is the targets’ <identifier X> (we will get to this) - Start our own login process and create a valid
code
- Replace the middle part of our token with <code from step 1> to impersonate them
- Profit
At this point we assume the middle code
part references the user in some way. However, lets confirm this by checking the source:
code.split('.')[0]
: In line 3 we see that the first part is theID
of the OAuth2Code.code.split('.')[1]
The second part (our injection point) contains the user session (UUID). (Now we know: <identifier X> == user session ID)code.split('.')[2]
The third part contains the client ID of the current session
1public static String persistCode(KeycloakSession session, AuthenticatedClientSessionModel clientSession, OAuth2Code codeData) {
2 [...]
3 String key = codeData.getId();
4 return key + "." + clientSession.getUserSession().getId() + "." + clientSession.getClient().getId();
5}
As an exercise for the reader, you can check in the commit how the user session is never validated by Keycloak, and then used to create an access token for the user session.
Exploitation
After we know what and where to inject, lets leverage this vulnerability to escalate our privileges within a Keycloak realm.
However, first of lets clarify the “realm” concept: Simplified a realm is a namespace in which users and clients exist. By default, Keycloak contains a master
realm which contains at least one administrative user and multiple management clients.
An organization might configure one central Keycloak instance with multiple realms. Lets say they provide a security-scanner for their customers. They will configure the realm security-scanner
, which contains multiple clients (scanner-shop
, scanner-api
, scanner-dashboard
, test
).
Pre-condition: A valid account is required
Exploits which require a valid attacker-controlled account are often quite restrictive. However, self-service account registration is a common business-case, especially for shops or public user portals which make use of authorization services like Keycloak.
Considering the previous example:
Clients can go through a self-register process which creates them an account within the security-scanner
realm.
After the self-registration they can authenticate themselves against Keycloak, they gain an access_token
from Keycloak and present the token to access the client scanner-api
.
This business-case is quite common and fulfills the exploit pre-condition “an attacker requires a valid account”.
Restriction: Scope of a user session
During the analysis of the vulnerability in our lab environment we noticed that we are not only bound to the realm of the user, but also to the client
scope of the user session. This is illustrated within the following screenshot, where a user has two different client sessions each with their own client (account-console
and test
). For an attacker this is a bad scenario, as in order to elevate their privileges they want to target a client session which contains multiple “clients”.
Such a “bad” scenario is not always the case. Depending on how the user accesses different services, and how they use the OAuth capabilities of Keycloak, they might end up with multiple OAuth clients registered to the same session:
Pre-condition: A client user session ID of a victim is required
This pre-condition is quite tricky. The client user session ID is a UUIDv4, which cannot be brute forced. In a real-world scenario this requires us to find a way to leak this ID.
There are multiple attack scenarios in which this can happen. In one scenario an attacker tricks a user into triggering a Keycloak OAuth flow on an attacker-controlled page. For example, through a page which uses Keycloak for login. It might also possible to extract a JWT token (which also contains the users session ID) via Cross Site Scripting (XSS).
After a successful login the attacker needs an additional exploit step in which they trick the user into accessing another Keycloak resource. For example, the keycloak “Admin Console” (admin/master/console
). As the victim is already logged into Keycloak and sends the necessary Cookies for an “interaction less” OAuth flow, they automatically add the security-admin-console
OAuth client to their session.
The attacker can then abuse CVE-2023-0264 to attach to the administrative session with their own low privilege Keycloak user. You can find the session attachment visualized in the following video. Of course, in a realistic attack scenario you will need to be a little more creative than just getting the victim to copy-paste a URL into a new tab.
- The upper browser displays the current sessions of the “victim” account.
- The victim (lower browser) uses OAuth to log into an attacker-controlled page which uses the Keycloak client
test
. Afterwards they access the admin console to attach thesecurity-admin-console
to the user session)
In the end the user session (as seen in Keycloak) will be structured similar to this:
The flow from the previous video is illustrated within the following flow diagram.
If you’re not a red teamer and just need a proof of concept in a regular penetration test, or within a bug bounty program, the whole exploit scenario gets a lot easier:
- Register two accounts
- Login with account 1 and get an
access_token
- Get the
session_state
claim within the token from step 2 - Perform the exploit with account 2
The following JSON object represents the JWT payload of a typical Keycloak access token (as described in the previous “Step 2”). Highlighted within the JSON object you can see the session_state
which is included within the Keycloak access_token
.
{
"exp":1681401821,
"iat":1681401761,
"auth_time":1681401761,
"jti":"524b3553-ee66-4cf6-b956-03027a308e44",
"iss":"http://test.keycloak.local:8060/realms/master",
"sub":"556da7c4-aaf0-43a9-9846-72ded642a777",
"typ":"Bearer","azp":"security-admin-console",
"nonce":"ae3b9770-e1ec-42e4-8f3f-1296d94d8b15",
"session_state":"81dfb556-7a1a-440d-a59e-58b7dd2c0f90",
"acr":"1",
"allowed-origins":["http://test.keycloak.local:8060"],
"scope":"openid profile email",
"sid":"81dfb556-7a1a-440d-a59e-58b7dd2c0f90",
"email_verified":false,
"preferred_username":"victim"
}
Session states can be leaked through other means as well. As illustrated in the following screenshot, Keycloak sets a cookie AUTH_SESSION_ID_LEGACY
which contains the current client session.
Privilege Escalation
Now to the (quite simple) exploitation part of this CVE. We illustrate how an attacker can abuse the vulnerability to attach themselves to a session of another user.
In our test environment the URL to the Keycloak account management page is as following:
To set up the exploit we need to prepare an administrative session to which we can attach to.
For this we log into the admin console with our account mogwailabs_admin
. During the login Keycloak appends the OAuth client security-admin-console
to our current session “af52ab74-d534-4d58-b3ed-f89d7a73a22b”.
After Keycloak appended security-admin-console
to our legitimate admin session, we of course retrieve the access_token
from Keycloak. The following screenshot illustrates how we query the Keycloak /whoami
endpoint with our newly acquired token. As already explained, the token contains our session_state
“af52ab74-d534-4d58-b3ed-f89d7a73a22b” which will be needed in the next exploitation step.
(Of course, in a realistic attack scenario the attacker would need to find a more sophisticated way to gain access to the session_state
.)
With the session state of our victim, we can exploit CVE-2023-0265 by sending a maliciously crafted code
to Keycloak. We create the code
by logging in with our attacker-controlled user “lowpriv”, and change the middle part of the code
(as described in the patch analysis). The exploit is illustrated within the following video and flow chart.
The exploitation flowchart extends the previous one by the remaining exploitation steps. Please note how the session_state
, which was retrieved during the early authentication steps by the legitimate user, is injected within the second-to-last step in the diagram.
As we sucessfully impersonated the admin, and we aquired their access token we can freely use the Keycloak admin console (or whatever client you have access permissions for).
Closing Thoughts
Given the CVSS rating, and the first impression of the vulnerability patch this CVE appeared quite severe to us. This was the main reason why we decided to write a vulnerability spotlight for this CVE. However, on second look a realistic attack scenario appears difficult, as it can be quite complex to gain access to session state of another user. Nonetheless, gaining administrative access to a Keycloak instance is devastating for an organization, and might be worth the trouble for some red teamers.
Penetration testers and bug bounty hunters might be sad when they can’t easily leverage this vulnerability into a PoC which actually takes over an administrative account. However, it is still a quick and easy win, with a severe vulnerability in the report (or a nice payout).
On a site note, even though Keycloak is quite transparent with vulnerabilities through GitHub Security, we noticed that their release blog rarely mentions security-relevant patches. This might result in some administrators missing important updates for their Keycloak instance.
Thanks to Cécile Brasseur on Unsplash for the title picture.
Keycloak website (2023): https://www.keycloak.org/ ↩︎