xss-image

// TL;DR


Hello friends — here’s an interesting finding I want to share.

The invite

About two months ago I received a private invitation on Bugcrowd, and the next day I started looking for bugs. There was only one target in scope — a testing site — and they provided me some credentials.

I can’t disclose the site name, so let’s assume https://redacted.com.

First of all, I logged in to the site with the provided credentials and inspected it for around fifteen minutes, just trying to understand how it worked. After that I started looking for bugs, and found multiple — mostly stored XSS. That day I found a total of eleven vulnerabilities. I reported them and received a bounty on each report, so I’m not disclosing the bounties. I’m also not sharing all of my findings here, just some of them.

First recon: password & auth

At the beginning, I tried to change my default password and successfully changed it to 1 — which was the first bug: there’s no password policy.

After that I checked some session management and rate-limiting issues and found nothing. Then I just copied my profile pic URL and opened it in incognito / a private window. The site prompted basic authentication to log in, which allowed me to brute-force the users’ login — that was my second finding.

After playing with the application for a while, I realised there were great chances of XSS.

Stored XSS via HTML file upload

So I found my first simple stored XSS by uploading an HTML file — there’s a feature that allows users to upload an HTML file and share it with others (team members).

Stored XSS in the report feature

Then I found my second stored XSS in the report feature, which let me create a report with HTML, resulting in stored XSS.

bounty stored xss

Reflected XSS: breaking out of href

Then I found my third one — a reflected XSS in a parameter (href).

Wait… it was not easy. I spent time on each vulnerability, because the application allowed HTML at certain endpoints but there were strong filters in place.

There was an href parameter in a URL, so I just changed its value to http://evil.com and it showed a link in the response, reflected inside an anchor tag in the href attribute:

<a href="reflection">Link</a>

So I changed the value to javascript:prompt(0), jaVascript://evil.com, and other protocols — but none were allowed; only a URL with http was allowed. Then I tried to break the href param with double quotes like this:

http://evil.com/">

…but it wasn’t possible to break the href attribute either. At first I was thinking there was no XSS. Then I again carefully inspected the source code and the DOM with Ctrl+U and Ctrl+Shift+I, and saw that the keyword was also reflecting inside JavaScript. So without wasting a single minute, I entered the payload '-confirm(3)-' in the href param value like this:

href=http://evil.com'-confirm(3)-'

…and boom, XSS fired in the browser.

I spent approximately two to three hours finding the above vulnerabilities. After that I took a little break and thought about stopping and getting some rest.

After a few minutes I changed my mind — don’t stop, keep going — so I turned my laptop back on and started looking for XSS in multiple places, and found six more. Now I’m sharing the three most exclusive findings.

Stored XSS in web link (+ the bypass)

There was a feature that allows users to add a URL, which other users can view (like a website URL on the profile page). So I entered javascript:prompt(4) in the URL field and the site showed an error, “URL must start with http or https” — so it wasn’t allowed to start the URL with any other protocol.

So I entered http://evil.com in the URL field, clicked Submit, then intercepted the request with Burp and changed the URL from http://evil.com to javascript:prompt(3), and it was successful. Then I saw that the payload became xxxxxxxxx:prompt(3) — meaning javascript was replaced with xxxxxxxxx.

So I repeated the steps and changed the URL to jaVasCript:prompt(4) — nothing happened with the uppercase. After trying some payloads with encoding I figured out the payload: jaVascript&colon;confirm(3).

The steps were:

  1. Entered the valid URL http://evil.com in the URL field and clicked Submit.
  2. Intercepted the request with Burp and changed http://evil.com to jaVascript&colon;confirm(3).
  3. Forwarded the request and turned off the intercept.
  4. Clicked the link — XSS successfully executed.

Bypass (after they fixed it). I repeated the previous steps and changed the URL to:

http://evil.com/"><script>alert(3)

…and saw the URL in the response as:

http://evil.com/&quot;&gt;alert(3)

…meaning it was stripping the <script> tag. So the final payload became:

javascript<script>:prompt(document.domain)

…and it was successful, bypassing the previous patch. The app removes <script> from the middle of the string, which reassembles a valid javascript: scheme.

Stored XSS in messages

The application had different roles for users, high to low privilege. For example:

  • Instructor (high privilege / admin)
  • Student (low privilege)
  • and more.

There was a message feature that allows students to message instructors and instructors to message students, with HTML. So I sent a message with multiple payloads like:

<a href="javascript:alert(1)">Click Me</a>
<a href="http://evil.com" onevent=XSS>Click Me</a>

…from a student (low) to an instructor (high) — but nothing happened, because the javascript scheme, <script> tag, and the event handler all got filtered. I tried almost everything, from encoding to a bunch of payloads, and was thinking it wasn’t vulnerable anymore.

After that, I repeated the same steps and sent the message with the payload <a href="javascript:alert(1)">Click Me</a> from the instructor to the student account — and it was successful. So an instructor can perform an XSS attack on students by sending a message. The report was rated P3.

Stored XSS in user email

This is my special finding, which was rated P2. I found a stored XSS in a user’s email through which a student (low) can perform a Cross-Site Scripting attack on an instructor (high).

An instructor (high) can view all team members, including a student (low), by name and email, and can edit the role and remove members in settings.

At that time I thought: previously I found an XSS with a payload in the name field, and it got triggered in the admin account. So I changed the student account name to something evil with the payload — but nothing happened, due to proper input sanitisation. The name field wasn’t vulnerable anymore.

Then I was thinking it’s not vulnerable, because the only way to XSS was by changing the name. I asked myself: what about the email? If we change the email to something evil, maybe it’s possible to XSS with the email.

Whenever I entered special characters in the email field, the site said it’s not allowed. So I just entered hacker@mail.com, clicked Save, intercepted the request with Burp, and changed it to:

hacker@mail.com"> ontest="test"

…and it was changed successfully. I quickly checked the student email from the instructor account and saw hacker@mail.com" ontest="test" in the email, and in the source code:

<a href="mailto:hacker@mail.com" ontest="test">hacker@mail.com" ontest="test"</a>

So I quickly repeated the steps and changed the email to hacker@mail.com" onmouseover="prompt(3)", and checked the response from the instructor account — onmouseover=prompt(3) was replaced with xxxxxxxx=prompt(3).

I repeated the steps multiple times and tried a number of payloads with different encodings, but nothing worked.

After that, I took a deep breath, drank some water, then traced my steps by following the previous step, and got an idea: the application strips an angle bracket. So the final payload became:

hacker@mail.com" <onmouseover>="prompt(1)"

…and when I moved the mouse cursor over the student email from the instructor account to check the code, XSS fired.

Takeaways

  • Map every reflection: Always check the reflection everywhere, in the source code and in the DOM. Note the total number of reflections and where each one is reflecting in the response
  • Treat client-side validation as a suggestion: Don’t worry if you get errors like “not allowed”. Just enter the allowed characters and submit the request, then intercept it with Burp and modify it.
  • Test across privilege levels: Check with different privilege levels, because sometimes the application doesn’t sanitise user input on the higher privilege.
  • A substring filter can be a weapon: Inspect carefully what is reflecting and what characters are being stripped, note the response, and craft the payload accordingly — a filter that strips a substring from the middle of your input can reassemble a valid payload.

Thanks for reading. I hope you liked this sharing. Have a nice day — happy hacking!

Anas Mahmood

Web & API penetration tester and bug bounty hunter. I write up the bugs I find and the methods that found them.

Leave a Reply

Your email address will not be published. Required fields are marked *