In this Post, we will describe a vulnerability in Drupal's OpenID SSO module that was shipped with Drupal Core prior Versions 6.30 and 7.26. The attack allows an attacker to login as an arbitrary user (even as an Admin), but does not require any interaction with the victim. The vulnerability was reported to the Drupal Security Team and they fixed it at the beginning of 2014 (SA-CORE-2014-001).
To detect the vulnerability, we developed a novel SSO attack technique called Key Confusion. We discovered the attack by setting up our own IdP for analyzing and attacking SSO, see Part 1 of our SSO attack series.
Key Confusion Attack on SSO
In Single Sign-On protocols like OpenID, OpenID Connect, SAML or BrowserID, the SSO token that is transferred from the IdP to the SP via the user's browser is cryptographically protected. In the case of OpenID, a HMAC is used.
Because decentralized SSO consists of multiple IdPs, the question that raises is:
How does the SP pick the right key to verify the token?
The basic idea of the Key Confusion attack is to let the SP use a key to verify the token, which SP believes to belong to honest Party (e.g. Google, Yahoo), while in fact, this key belongs to the attacker. The attacker has therfore to setup his own IdP and we refer to it as “malicious IdP”.
The following figure depicts the concept:
Figure
1: Key Confusion concept.
- The attacker starts the OpenID login procedure by entering
his own URL.ID=attacker.
- The SP gathers the URL.IdP
of the attacker IdP by discovering URL.ID=attacker.
The attacker uses his malicious IdP (i.e. the IdP is controlled by
the attacker) to establish a key β with the SP. This takes place in
the association phase of OpenID, see
our
blogpost on the OpenID protocol description
and is conform to the normal OpenID protocol flow.
- The SP stores the established key in its database. It uses
the Key ID β in this example. Note that in OpenID, the Key ID value
is defined by the (malicious) IdP. That means, it could be possible
for a malicious IdP to overwrite existing key material. For the
attack on Drupal as described on this post, the overwriting of key
material is not needed.
- The attacker sends an SSO token to the SP. For simplicity
reasons, we leave out the token creation here and some of the
protocol messages are omitted in the Figure shown above, e.g. the
initial request sent from the SP to the IdP and the authentication
of the user.
- The SP proceeds with the verification of the token's
signature σ. It therefore needs to fetch the key with Key ID β as
requested in the token. Because of the value URL.IdP=victim
contained in the token, the SP believes that the key β belongs to
the victim's IdP, but actually it belongs to the malicious
IdP (see Step 1).
Key Confusion: Summary
- SP
chooses the wrong key to verify the signature in the SSO token.
- The
SP permits the access to victim's account.
Attack:
- The
attacker sends token t
= (URL.ID=victim, URL.IdP=victim, β),
where β belongs to a key controlled by URL.IdP=attacker
(instead of URL.IdP=victim).
Breaking OpenID in Drupal... The Code
01. if (openid_verify_assertion($service, $response)) {
02. if ($response['openid.claimed_id'] != $service['openid.claimed_id') {
03. $discovery = openid_discovery($response['openid.claimed_id']);
04. return openid_idp_uri_in_discovery($discovery, $service['openid.op_endpoint']);
05. }
06. }
First, a short explanation of what variables are used in this code:
- $response
contains the OpenID token that is sent
to Drupal (cf. Step 4 in Figure 1).
- $service
holds information about the discovered
URL.ID that was sent in
Step 1, Figure 1.
Line 1 verifies the token. This includes the token's signature verification. If the verification succeeds, Line 2 checks if the URL.ID parameter contained in the response matches the URL.ID parameter that was previously requested (Step 1, Figure 1). If this is not the case, a rediscovery is started in Line 3 on the URL.ID parameter in the response. Finally, Line 4 verifies if the discovered document at URL.ID contains the matching URL.IdP.
We started the Key Confusion attack. First, we sent to URL.ID=attacker Drupal as a login request. Drupal discovers URL.ID=attacker and associates a key β with our malicious IdP. We then sent a token t = (URL.ID=victim, URL.IdP=victim, β) to Drupal. Using the debugger, we observed that the token was successfully verified (Line 1 passed), that means, Drupal uses the key identified β. However Drupal noticed that the claimed identifier (URL.ID=victim) in the token t does not match the previously requested identifier (URL.ID=attacker). Thus, Drupal starts the rediscovery (Lines 2-3) on URL.ID=victim. Drupal fetches the URL.IdP= victim in the discovery. So what Line 4 basically does, is the following: It compares if URL.IdP= victim (Parameter: $discovery) matches URL.IdP=attacker (Parameter: $service['openid.op_endpoint']).
Note that in Line 4, Drupal does not use $response['openid.op_endpoint'] (the URL.IdP parameter contained in t) but $service['openid.op_endpoint'] (the URL.IdP parameter stored after the login request).
The main question to answer was: What exactly is $service and can we manipulate it?
We again found the answer in the Code (File: modules/openid/openid.module):
338 $service = $_SESSION['openid']['service'];
The variable value is taken from the PHP Session. We looked deeper into the code, and found out, that $_SESSION['openid']['service'] is initialized on Step 1. of our attack (cf. Attack Concept Figure).
Breaking OpenID in Drupal... The Exploit
Using the knowledge of the code we earned above, we created an exploit that is depicted in the following Figure:
Key Confusion Attack on Drupal. |
- Step 1-3: The attacker starts to login on Drupal. He
therefore submits his own attacker identity URL.IDA. The
SP then starts the discovery on URL.IDA to determine
URL.IdPA.
- Drupal stores $_SESSION[URL.ID]
=
URL.IDA
and $_SESSION[URL.IdP]
=
URL.IdPA
as session state.
- Drupal stores $_SESSION[URL.ID]
=
URL.IDA
and $_SESSION[URL.IdP]
=
URL.IdPA
as session state.
- Step 4: Drupal associates a key with URL.IdPA. The
key is stored with association handle β.
- Steps 5-7: Drupal redirects the attacker to URL.IdPA.
The malicious IdP creates a token t∗ = (URL.IDV
, URL.IdPV , URL.SP, β).
- The attacker delays here and does not proceed sending the
token back to SP.
- The attacker delays here and does not proceed sending the
token back to SP.
- Steps
8-10: The attacker submits a further login request to Drupal, but
this time with the victim’s identity URL.IDV. Drupal
starts a new discovery on it and receives URL.IdPV. Both
values, URL.IDV and URL.IdPV, are then stored
in $_SESSION,
overwriting URL.IDA and URL.IdPA .
- Step 11: Drupal starts another association. This time with
URL.IdPV. This association is stored with the association
handle α.
- Step 12: Drupal redirects the attacker to URL.IdPV.
The attacker stops here, because he does not know the credentials to
login at URL.IdPV. His goal is reached, the $_SESSION
variables are overwritten.
- Step 13-14: The attacker continues to forward Step 7 to the
Drupal SP. Drupal verifies the signature σ using association handle
β from t*. The signature is valid, and Drupal continues
with the code outlined above. Because $_SESSION[URL.ID]
is equal to $response[URL.ID],
Drupal does not start a rediscovery.
Conclusion
What can we learn from this attack?
A proper key management should always store the key's origin
Drupal fetches (and stores) the key material as follows:
$association = db_query("SELECT * FROM {openid_association} WHERE assoc_handle = :assoc_handle", array(':assoc_handle' => $response['openid.assoc_handle']))->fetchObject();
The problem with the code above is, that the key is only fetched by the variable openid.assoc_handle. This value does not include for which IdP this can must be used. A better way to store key material would be:
$association = db_query("SELECT * FROM {openid_association} WHERE assoc_handle = :assoc_handle AND url_idp = :url_idp", array(':assoc_handle' => $response['openid.assoc_handle'], ':url_idp' => $response['openid.op_endpoint]))->fetchObject();
This method connects both, the key identifier and the origin. After contacting the Drupal Security Team, they applied the behavior above and Key Confusion was no longer possible.
Connecting HTTP requests via Session variables can cause problems
A concept that the attack on Drupal reveals, is that the use of variables stored as session parameters cannot be handled as trustworthy input for cryptographic computations. HTTP by itself is a stateless protocol and the use of cookies/sessions are the concept of connecting a sequence of requests. However, when it comes to security, one should not rely on these parameters. If, for example, Drupal had started a rediscovery every time, the attack would not be possible.
An interesting fact on our Key Confusion attack is related to our previous post that attacks OpenID on Sourceforge with ID Spoofing. The Sourceforge Security Team fixed the ID Spoofing vulnerability, but we then tried out the Key Confusion Attack in the same manner as on Drupal. Sourceforge was vulnerable. We then contacted the Sourceforge Security Team again, and they fixed the issue on a similar way as Drupal.
Authors of this Post
Vladislav MladenovChristian Mainka (@CheariX)