Schwachstellenanalyse: CVE-2023-0264
Analyse einer Impersonierungs-Schwachstelle in Keycloak
- Name
- Timo Müller
Dies ist eine übersetzte Version. Das englische Original finden Sie hier.
Keycloak is an open source identity and access management solution […] [to] secure services with minimum effort
Bei Keycloak handelt es sich um einen verbreiteten Authentifizierungsserver, der von vielen unserer Kunden verwendet wird. Als Teil unserer Arbeit analysieren wir regelmäßig aktuelle Meldungen zu Schwachstellen, um unseren Kunden notfalls auf wichtige Sicherheitsupdates hinweisen zu können. Im Zuge dessen schauten wir uns auch die Keycloak-Schwachstelle CVE-2023-0265 (CVSS Bewertung8.3) im Detail an.
In diesem Blogpost analysieren wir diese Schwachstelle, die (unter bestimmten Voraussetzungen) die Impersonierung eines anderen Keycloak-Benutzers erlaubt. Im Internet finden sich hierzu nur sehr wenige Informationen, wir wollen daher die Schwachstellen-ursache analysieren und die Ausnutzung an einer ungepatchte Keycloak Instanz demonstrieren.
CVE-2023-0264 Management Summary
Die offizielle Beschreibung für CVE-2023-0264 ist recht allgemein gehalten. Sie sagt das “ein Benutzerkonto impersoniert werden kann”, wenn den Angreifern eine “UUID” bekannt ist. Basierend auf der offiziellen CVSS Bewertung muss für eine erfolgreiche Ausnutzung eine Interaktion mit dem potenziellen Opfer erfordert. Hier die offizielle CVE-Beschreibung:
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.
Sämtliche Keycloak Instanzen vor 21.0.1
sind von der Schwachstelle betroffen. Betroffene Installationen sollten schnellstmöglich aktualisiert werden.
Patch-Analyse
Das von GitHub gereviewte Advisory verweist auf das relevante Git Commit. Dieser Commit enthält glücklicherweise nur die zur Behebung der Schwachstelle gehörenden Änderungen, was eine Analyse vereinfacht.
Schauen wir uns zuerst die Generierung eines OAuth Codes an. Der Patch erweitert den Constructor der zuständigen Klasse um das zusätzliche Argument 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 )
Für eine Usersession werden Cookies, oder eine andere Art der Zustandsidentifikation benötigt. Dies schließt daher schon alle OAuth Flows aus, die nicht Session-gebunden sind, beispielsweise den Device Authorization Grant (RFC 8628).
Die Analyse des mit den Patch implementierten Testcases liefert uns weitere wichtige Ansatzpunkte:
Der Testcase führt zunächst eine “normale” OAuth-Anmeldung für zwei unterschiedlichen Testkonten durch (Zeilen 4, 10). Der Login-Flow erhält zwei code(s)
(Zeilen 5, 11), die im Anschluss verwendet werden um Accesstokens für die beiden Accounts zu erhalten (Zeile 20).
Der code
besteht aus drei Teilen, die jeweils durch einen Punkt (.
) getrennt werden. In Zeile 12 bis 15 wird der mittlere Teil des ersten Codes ausgelesen und der entsprechende Teil des zweiten Codes damit ersetzt.
Abschließend wird versucht mit dem so manipulierten Code eine Accesstoken zu erhalten (Zeile 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}
Aus diesem Testcase kann man den ungefähren Ablauf eines erfolgreichen Angriffs ableiten:
- Zugriff auf den mittleren “Code” Teils eines anderen Nutzers. Wir nennen diesen Teil vorübergehend <identifier X>
- Start eines eigenen Loginvorgangs um einen gültigen
code
zu generieren. - Ersetzen des mittleren Teils unseres Tokens mit <Identifier X aus Schritt 1> um diesen Account zu impersonieren.
- Profit
An diesem Punkt nehmen wir an, dass der mittlere Teil eines codes
auf die ID des Nutzers verweist. Wir können das durch Analyse des Quellcodes verifizieren:
code.split('.')[0]
: Der erste Teil ist dieID
des OAuth2Codes.code.split('.')[1]
Der zweite Teil (den den wir ausstauschen)ist die Session ID des Nutzers in Form einer UUID. <identifier X> == user session ID)code.split('.')[2]
Der dritte Teil beinhaltet die ClientID der aktuellen 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}
Die Schwachstelle wird dadurch verursacht das diese Usersession von Keycloak vor dem Erzeugen eines Accesstokens nicht geprüft wird.
Ausnutzen der Schwachstelle
Nachdem wir jetzt wissen was wir wo austauschen müssen können wir uns ansehen wie wir hierdurch unsere Rechte in einem Keycloak Realm erweitern können. Beim “Realm” Konzept von Keycloak handelt es sich um einen Namensraum in dem Benutzer und Clients existieren. Standardmäßig besitzt Keycloak einen master
Realm mit mindestens einem administrativen Benutzer sowie mehreren zum Management verwendete Clients.
Eine Organisation oder Firma kann mehrere Realms in einer zentralen Keycloak Instanz konfigurieren. Nehmen wir als Beispiel den Anbieter eines Schwachstellenscanners. Dieser besitzt einen security-scanner
Realm mit mehreren Clients (scanner-shop
, scanner-api
, scanner-dashboard
, test
)
Voraussetzung: Gültiges Benutzerkonto
Schwachstellen, die Zugriff auf ein gültiges Benutzerkonto vorraussetzten sind oft schwerer ausnutzbar. Es existieren jedoch mehrere Szenarien bei denen auch externe Angreifer sich einen solchen Benutzer erstellen können, insbesondere bei Shops oder Benutzerportalen die Keycloak verwenden. Dieser erlauben häufig die Registrierung eigener Kundenaccounts.
In unserem vorherigen Beispiel könnten Angreifer beispielsweise über einen Testaccount registrieren, wodurch ein entsprechendes Konto im security-scanner
Realm von Keycloak erzeugt wird.
Nach der Registrierung können sich die Angreifer per Keycloak authentifizieren. Dadurch erhalten Sie einen Keycloak access_token
und können mit diesem auf den Client scanner-api
zugreifen.
Dieser Businesscase ist recht verbreitet, insbesondere bei im Internet erreichbaren Anwendungen mit vielen Nutzern.
Voraussetzung: Scope der Benutzersession
Bei der Analyse der Schwachstelle in unserer Testumgebung stellten wir fest, das die Session nicht nur an Realm des Nutzers, sondern auch an den client
Scope beschränkt sind. Das wird im folgenden Screenshot gezeigt, wo ein Nutzer zwei separate Sessions, jede mit jeweils einem Client (account-console
und test
) hat. Für Angreifer ist das eher ungünstig, die Session des Opfers sollte idealerweise Zugriff auf mehrere “clients” erlauben. Angreifer können dadurch auch im Kontext des Opfers auf diese Clients-Zugreifen.
Je nachdem, wie die Applikation und OAuth verwendet wird kann es durchaus vorkommen, dass ein Nutzer mit derselben Session auf mehrere OAuth-Clients zugreift.
Voraussetzung: Kenntnis der SessionID des Opfers
Diese Voraussetzung klingt recht schwierig. Bei der SessionID handelt es sich um eine UUIDv4 die sich nicht brute-forcen lässt. In einem realen Angriffsszenario benötigen wir daher einen anderen Weg um an diese ID zu kommen.
Der Einfachheit halber nehmen wir in unserem Beispiel an, dass der Angreifer sein Opfer dazu bringen kann eine von ihm kontrollierte Seite per Keycloak anzumelden. Denkbar wäre auch eine Cross Site Scripting (XSS) Schwachstelle in einer Anwendung, da sich die SessionID auch beispielsweise aus einem JWT Token auslesen lässt.
Nach einer erfolgreichen Authentifizierung benötigt der Angreifer einen zusätzlichen Schritt um sicherzustellen, dass sich das Opfer an einem weiteren Client, beispielsweise die Keycloak “Admin Console” (admin/master/console
) aufruft und sich dort ebenfalls authentifiziert. Da das Opfer bereits per Keycloak angemeldet ist ist und die für einen “interaktionslosen” OAuth Flow benötigten Cookies mitsendet wird der security-admin-console
OAuth Client automatisch zur Session hinzugefügt.
Die Angreifer können sich anschließend per CVE-2023-0264 mit ihrem niedrig privilegierten Keycloak-Nutzer an die administrative Session anfügen. Dies wurde im folgenden Video visualisiert. In einem realen Angriffsszenario muss man ein wenig kreativer sein um das Opfer zum Aufruf einer URL zu bewegen.
- Der obere Browserfenster zeigt die aktuelle Session des Opfer-Kontos in Keycloak.
- Das Opfer (unteres Browserfenster) meldet sich per OAuth auf der von den Angreifern kontrollierten Seite (Keycloak Client
test
) an. Danach erfolgt ein Zugriff auf die Admin-Konsole wodurch dersecurity-admin-console
Client zur aktuellen Session unseres Opfers hinzugefügt wird.
Die Benutzersession (aus Sicht von Keycloak) beinhaltet Final zwei Clients:
Der Ablauf des vorherigen Videos ist im folgenden Diagram illustriert:
Ist man kein Red Teamer und benötigt beispielsweise im Rahmen eines regulären Penetrationstests oder Bug Bounty Programms ein Proof of Concept ist die Verifikation der Schwachstelle deutlich einfacher:
- Registrierung von zwei Benutzerkonten
- Anmeldung mit Account 1 um einen
access_token
zu erhalten. - Auslesen des
session_state
Claims mit dem im vorherigen Schritt erhaltenen Token - Ausnutzen der Schwachstelle mit Account 2
Das folgende JSON Objekt zeigt den JWT Payload eines typischen Keycloak Accesstokens den man im “Schritt 2” erhält. Die hervorgehobene Zeile ist der session_state
, welche für den Keycloak access_token
benötigt wird.
{
"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"
}
Die session_state
UUID eines Benutzers kann auch anderweitig kompromittiert werden. Keycloak setzt ein AUTH_SESSION_ID_LEGACY
Cookie welches ebenfalls die aktuelle Client-Session enthält (siehe nächster Screenshot).
Rechteerweiterung
Kommen wir nun zum (recht einfachen) Ausnutzen dieser Schwachstelle. Wir zeigen wir Angreifer diese Schwachstelle ausnutzen können um die sich selbst an die Session eines anderen Nutzers anzuhängen.
In unserer Testumgebung lautet die URL des Keycloak Account Managements wie folgt:
Für unser Testszenario benötigen wir zunächst eine administrative Session die wir kompromittieren können. Dazu melden wir uns an der Admin-Console mit dem Account mogwailabs_admin
an. Durch die Anmeldung fügt Keycloak den OAuth Client security-admin-console
zu unserer aktuellen Session “af52ab74-d534-4d58-b3ed-f89d7a73a22b” hinzu.
(In einem realen Angriffszenario würden Angreifer vermutlich einen anderen Weg finden um an den session_state
des Opfers zu kommen, hierfür würde beispielsweise eine Cross-Site Scripting (XSS) Schwachstelle genügen)
Mit der Session State UUID des Opfers können wir nun die eigentliche Schwachstelle ausnutzen indem wir einen entsprechenden code
an Keycloak senden. Dazu melden wir uns mit dem vom Angreifer kontrollierten Account an und tauschen den zweiten Teil des code
, wie bei der Patchanalyse beschrieben, aus. Die jeweiligen Schritte sind im folgenden Video und Ablaufdiagram illustriert.
Das folgende Ablaufdiagram wurde um die fehlenden Exploiting-Schritte erweitert. Der session_state
des Opfers, den wir in den vorherigen Anmeldeschritten erhalten haben im vorletzten Schritt des Diagramms verwendet wird.
Nachdem wir erfolgreich den Administrator impersoniert und einen gültigen Accesstoken erhalten haben können wir mit diesem auf die Keycloak Adminkonsole (oder jeder andere Client für den wir Zugriffsberechtigungen besitzen) zugreifen.
Abschließende Anmerkungen
Wir gingen, basierend auf der CVSS Bewertung sowie einer ersten Patchanalyse, zunächst von einer schwerwiegenden Schwachstelle aus. Das war auch der Grund warum wir diese überhaupt intensiver analysiert haben.
Es stellte sich aber heraus das die praktische Ausnutzung dieser Schwachstelle in einem realen Angriffsszenario recht schwierig ist. Hierfür müssen Angreifer Zugriff auf den Session-State eines anderen Nutzers erhalten, was beispielsweise das Ausnutzen einer weiteren Schwachstelle erfordert. Dennoch, die Möglichkeit eine administrativen Zugang zu einer Keycloak Instanz zu kompromittieren kann den notwendigen Aufwand für Angreifer rechtfertigen, da die Konsequenzen für eine Organisation verherend sein können.
Penetrationstester und Bugbounty Hunter werden vielleicht enttäuscht sein, da sich diese Schwachstelle nicht einfach in ein PoC umwandeln können um damit beispielsweise die Konten von Administratoren zu kompromittieren. Es handelts sich aber dennoch um eine schwere Schwachstelle, die sich gut in einem Report macht (oder einen kleine Bounty gibt).
Keycloak ist bei Schwachstellen durch GitHub Security Advisories transparent, uns viel aber auf das ihr Release Blog nur selten security-relevante Updates erwähnt. Einige Administratoren könnten daher eventuell wichtige Updates für ihre Keycloak Instanzen übersehen.
Danke an Cécile Brasseur bei Unsplash für das Titelbild.