Stored XSS into onclick event with angle brackets and double quotes HTML-encoded and single quotes and backslash escaped

2 min read Easy PortSwigger
XSS
Contents

On this page

The lab

Here is how PortSwigger describes it

This lab contains a stored cross-site scripting vulnerability in the comment functionality.

To solve this lab, submit a comment that calls the alert function when the comment author name is clicked.

The idea#

The site is escaping our input at the JavaScript layer, watching for real quotes and real backslashes and defusing them. But an HTML entity like ' is not a quote as far as that escaper is concerned, it is just six harmless looking characters, so it walks straight past untouched. Then, a moment later, the browser does its HTML decoding pass and turns ' back into a real single quote, right before the JavaScript runs. We are handing the site something that only becomes a quote after it has finished checking for quotes.

Step 1 - find where the comment lands#

The lab tells us it is stored and in the comments, so I opened a post, scrolled to the comment form, and submitted one with a website filled in. The author name comes back as a link, and our website value shows up in two places on it.

11
HTML
<a id="author" href="OURSITE" onclick="...tracker.track('OURSITE')...">logicbreaker</a>

The href is one copy, but the interesting one is the second, inside the onclick. Our website is dropped into a little tracking call, sitting inside a single quoted JavaScript string as the argument to tracker.track('...'). That onclick is JavaScript that runs when the name is clicked, which lines up exactly with what the lab wants, an alert when the author name is clicked. So the website field is really an injection point into that onclick code.

Step 2 - watch every normal breakout get shut down#

The obvious move is to close the tracker.track string with a quote and write our own code, so I tried a website of https://logicbreaker.sh' + alert(1) + '. It does not work, the site puts a backslash in front of our quote and it stays trapped inside the string. Reaching for a double quote to break the attribute is no good either, since double quotes are HTML encoded, and so are angle brackets, so we cannot spawn a fresh tag or script. Every straightforward exit named in the lab title is genuinely blocked.

That is the cue to stop sending a literal quote and send something that is not a quote yet.

Step 3 - smuggle the quote in as an entity and solve#

HTTP
http://logicbreaker?&apos;-alert(1)-&apos;

The &apos; is the HTML entity for a single quote. When the site does its JavaScript escaping, it sees no real quote to escape, because at that stage &apos; is just plain text, so it leaves our payload completely alone. The value gets stored and rendered into the onclick. Then the browser parses that onclick attribute, HTML decodes it, and both &apos; turn into actual single quotes. The JavaScript that the browser is left holding reads like this.

javascript
tracker.track('http://logicbreaker?'-alert(1)-'')

Follow it through. The first decoded quote closes the 'http://logicbreaker?' string early. Then -alert(1)- is arithmetic, so the browser has to evaluate alert(1) to do the subtraction, and that is our payload firing. The last decoded quote pairs with the closing quote the template already had, leaving a harmless empty string on the end, so the whole line stays valid JavaScript with nothing dangling.

22

With this, the lab is solved!