Introduction

While testing a target, I found a self-XSS. On its own, this vulnerability has no impact. However, if chained with other gadgets, it may be possible to achieve account takeover. In this post, I explain how I was able to do it.


Self-XSS

I was assessing the main application of a target and believed I had already exhausted most of the attack surface. The application was relatively small, although with some sensitive features, and I had skimmed most of the JavaScript code without finding significant sinks. I had found some interesting innerHTML, and although it just seemed like this application would render something into innerHTML without sanitization, I couldn't find any source to reach them.

I was about to move on when I decided to take a last look at the account settings. More specifically, I clicked Edit on the phone number settings page and to my surprise, the UI for editing an existing phone number was very distinct from the UI used to add a new one. I had already tested this and the server did not like injected non-alphanumeric characters. So I tested it again, and indeed, the API endpoint to update the phone number was also different. So, as you can guess, I sent the most common payload of all, and my browser popped an alert.

<img src=x onerror=alert(1)>

Ok, cool. But this is nothing more than a self-XSS, which will only execute on the phone number settings page of the user who saves the payload. With no way to force another user to view my profile data, the impact was slim to none.

To complicate matters, the application used two distinct authentication cookies, both marked as HttpOnly.


Login CSRF

Since I could not force a victim to view my profile data, and thus trigger the XSS, I started to investigate the next obvious step: login CSRF. Luckily, the login request had 2 things in my favor:

  1. The Content-Type was application/x-www-form-urlencoded
  2. The URL had a ReturnUrl parameter which, as you can guess, redirects the user to a certain endpoint after the login.

As such, despite the presence of various .NET parameters in the request, a simple PoC using the form tag and document.getElementById('csrf-form').submit(); did the trick. By opening the PoC page, I was logged in to my account, redirected to the phone number settings, and the XSS was triggered.

Additionally, if I added window.location.href to the XSS and assigned the /logout endpoint to it, it would automatically log me out and redirect to the login page. This will be useful for later.

Ok, cool again. But I am still very limited. Triggering XSS is useless if the victim is logged into our account rather than their own.

And to complicate matters again, all of the pages in the application came with X-Frame-Options: SAMEORIGIN, which means it is not possible to iframe them.


So, at this stage, I had the following:

  1. With the login CSRF, the victim logs into the attacker's account
  2. Victim lands on the phone number settings page and executes the XSS payload

As I said, the problem is the context. The XSS will execute inside the attacker's session, which is useless, because if I try to exfiltrate sensitive data, it will just return the attacker's data. But, this is where cookie tossing comes in.

As is well known, browsers determine which cookies to send based on the Path attribute. As such, if there are two cookies with the same name, the browser will prioritize the one with the more specific path. For example:

  • SessionId=Victim; Path=/;
  • SessionId=Attacker; Path=/settings/phone;

In this scenario, the browser will prioritize the attacker cookies for the /settings/phone endpoint, while the victim's session will prevail on the rest of the application. If I could abuse the login CSRF to do this, and then log the victim out of my account, it could be possible to achieve XSS on the victim's session.

Cool! But there are two problems:

  1. It is not possible to read HttpOnly cookies, so I am unable to set them using JavaScript.
  2. Even if I was able to set them, they would just become invalid after the victim is logged out of the attacker's account.

HttpOnly Bypass

In short, I need valid attacker cookies planted on the victim's browser with a specific path constraint. But I can't read them client-side to "toss" them. So a workaround came to my mind.

I opened my browser in incognito mode and logged in, in order to confirm that it was possible to have multiple sessions active, which it was. So I thought: "Can I just log in to the attacker's account somewhere, grab the cookies, expose them somewhere publicly accessible and let the JavaScript fetch them?". And the answer was: yes.

I quickly grabbed my VPS and created a bash script that:

  1. Used curl to log into the attacker's account.
  2. Parsed the response headers and grabbed the cookies.
  3. Wrote them to a file and exposed it via an endpoint on my domain.

Then, I set it to run on a cron job every 30 minutes.

cookies

Cookies endpoint


XSS

The chain is starting to take shape. But there's one last piece missing: the actual XSS payload. It needs to understand when to do the cookie tossing, and when to exfiltrate information/do actions on behalf of the victim. To do this, I leveraged the / page, which displays the username of the logged in account, for context check. And coded the following logic:

  1. Fetch /
  2. If attacker in username:
    • Fetch fresh cookies from https://attacker.com/cookies
    • Set document.cookie using the attacker's valid cookies, but scope specifically to /settings/phone
    • Log out
  3. If attacker not in username:
    • This means the victim has the attacker's session for the XSS page, but all the other pages are utilizing the victim's session.
    • Execute the attack (exfiltrate data, perform actions, etc)

Full Chain

Now that I have everything together, the attack would go as follows:

  1. Victim visits https://attacker.com
  2. After being logged out, wait for the victim to log in again
  3. When the victim visits the phone number page, the XSS is triggered on the victim's account

Here is a diagram showing the entire flow:

Full attack flow

Full attack flow