It may sound like I'm simply testing authentication, but I'm actually deep diving through many JS files and iframes interacting with each other. Today, I want to cover a one-click account takeover bug that I recently found on a well known company's main website, which I'm 100% sure has been tested by many internal security teams and external hunters.
Before jumping into the finding, let me set the stage with some background on how OAuth flows work and why some setups are significantly harder to break than others.
A Quick Primer on SSO and OAuth
Single Sign-On (SSO) is the concept of entering your credentials once and getting access to multiple services. Google is a perfect example. You sign into your Google account, and suddenly Gmail, YouTube, Google Cloud, and everything else just works.
The implementation behind SSO varies. Some companies rely on well-known protocols like SAML or common OAuth providers. Others, especially large enterprises, build their own private SSO systems. And this is where things get interesting for a bug hunter, because custom implementations are far more likely to contain flaws.
Here is the typical OAuth login flow. You go to a website, click "Login with Google," and the following happens:
- The Application (e.g., YouTube) redirects you to the Provider (e.g., Google) with a set of parameters including
client_id,redirect_uri,scope, andstate - You authenticate with the Provider
- The Provider sends you back to the Application's
redirect_uriwith an authorizationcode(or token) - The application exchanges that code for an access token on the server side, then issues an authentication cookie, session, or token for the user
The critical security boundary here is the redirect_uri. If an attacker
can manipulate where the authorization code is sent, they can steal it, exchange it,
and take over the victim's account. This is why every OAuth provider validates the
redirect_uri strictly.
Now, there is a common misconception among fresh hunters. Many people focus on attacking the Provider side (Google, Facebook, etc.). But the Provider has been tested by millions of researchers. The real attack surface is on the Application side, specifically how the application handles the OAuth setup, the redirect, and the token exchange.
Two Types of Redirects
In OAuth implementations, there are generally two types of redirects after the Provider sends back the code:
- Exchange Endpoint: A server-side endpoint that receives the code and exchanges it for a token/authentication session/cookie. This is a 301/302 redirect, no JavaScript involved. The only attack vector here is manipulating the
redirect_uri - Dispatcher Endpoint: A client-side page that uses JavaScript to process the response and redirect the user. This is trickier because JS introduces additional attack surface, such as manipulating the
stateparameter, postMessage abuse, or Unicode tricks inwindow.location
The second type is more complex and often more likely to be vulnerable. I have
previously exploited dispatcher-based flows in one of Google's products and I
received $12,000 for it, I'll be publishing it in future. What if the
redirect_uri is the sole point to probe for vulnerabilities, and the
implementation appears completely secure?
That is exactly the situation I faced with this target :]
The Target's OAuth Flow
The target company is a major global brand in the automotive industry. They operate a unified OAuth system shared across multiple subsidiary brands, powered by a custom identity provider. When you click "Login" on any of their regional websites, the following OAuth request is made:
https://idp-connect.company.com/auth/api/v2/user/authorize
?client_id=dummy
&redirect_uri=https%3A%2F%2Fwww.company.com%2Fapi%2Fbin%2Fopenid%2Flogin
&state=[BASE64_STATE]
&scope=openid+profile+email+phone
&response_type=code
&ui_locales=en
After authentication, the provider redirects to:
https://www.company.com/api/bin/openid/login?code=AUTHORIZATION_CODE&state=...
This is the exchange endpoint. The Application receives the code and exchanges it for the user's session.
Analyzing the Attack Surface
I immediately started mapping what I was working with:
- Application: www.company.com
- Provider: idp-connect.company.com
- Exchange endpoint:
https://www.conpany.com/api/bin/openid/login - No dispatcher endpoint. No JavaScript involved. The redirect was a clean 301 HTTP redirect
- No
stateparameter abuse possible because the state was just a base64-encoded URL for post-login redirect, and the exchange endpoint was server-side
This meant the only viable attack was manipulating the
redirect_uri. If I could make the provider redirect the
authorization code to a domain I control, game over.
The issue? The redirect_uri validation was highly protected. However,
unlike the main Google provider (the famous one that many sites use it), it accepted
both subdomains and the main domain, so abc passed the security checker
function without any problems:
redirect_uri=https%3A%2F%2Fabc.company.com%2Fapi%2Fbin%2Fopenid%2Flogin
But hold on, how does a parser detect subdomains? Bingo! By parsing the URL or applying a regex to it. In this case, attackers can exploit inconsistencies across different layers, just as I did.
The Wall: Every Trick in the Book, Blocked
I started with the standard redirect_uri manipulation techniques. Every single one was blocked.
Path confusion with ? and #:
https://www.company.com?/api/bin/openid/login → Rejected
https://www.company.com#/api/bin/openid/login → Rejected
Domain substitution:
https://www.company.computer/api/bin/openid/login → Rejected
https://wwwa.company.com/api/bin/openid/login → Rejected
Authority injection with @:
https://[email protected]/api/bin/openid/login → Rejected
Domain concatenation:
https://a.coma.company.computer/api/bin/openid/login → Rejected
https://[email protected]/api/bin/openid/login → Rejected
https://a.com\@.company.computer/api/bin/openid/login → Rejected
Every standard bypass was dead. The validation was solid. At this point, most hunters
would move on. The redirect_uri was locked down. Nothing to see here.
But I do not give up after one round of attempts. I always push further.
Fuzzing the URL Parser
I changed my strategy. Rather than using familiar bypass techniques, I opted to fuzz the URL parser. The key question was whether characters could be decoded again after passing the server-side URL validator.
I started fuzzing encoded characters at various positions in the URL (