Recently I was happily busy solving the February 2022 Intigriti XSS challenge: this post wants to be a description of the chain of thoughts that brought to the solution.
I am going to anticipate the solution, which looks like this:
Table of contents
Requirements for the solution from Intigriti
- Should work on the latest version of Chrome and FireFox.
- Should execute alert(document.domain).
- Should leverage a cross site scripting vulnerability on this domain.
- Shouldn’t be self-XSS or related to MiTM attacks.
- Should be reported at go.intigriti.com/submit-solution.
- Should require no user interaction.
Investigating the problem
At loading time the JS from the challenge does a few things:
- defines the function showModal: it takes a string as parameter, which is injected in the HTML by innerHTML (the function is the target)
- looks for the URL parameter “q”: its value is passed to the exploitable function showModal (q is the entry point for the exploit)
- it selects the characters on the URL between “&q=“ and “&first=” (both excluded): that is the value of the showModal‘s parameter from 1.
- it defines the variable uri on the window: uri ‘s value is the location.href
The points above shape the goal as defining an HTML tag that can be injected as a string on the URL, able to run JS and that satisfies the points defined in the section “Requirements” of this post.
Limitations on the way:
- a check is in place for the URL’s parameter q, the entry point: the max length for its value is 24 characters
- the code from the tag <script> does not run when injected by innerHTML, so that is not an option
The max length of 24 characters for the payload is a big limitation, that is why during the challenge I have started to look for global variables defined in the window: the idea is to store the “code to be executed” somewhere else and then use the 24 characters of the payload to only evaluate the code. Sounds easier to say it than to do it.
If you remember, in point 4. above was said that the global variable uri has a value equal to location.href: I have discovered that variable at this point of the investigation (for necessity) and as a consequence, the URL clicked straight away as a place to store the exploit, for example as a URL parameter’s value. The idea was something like eval(uri.split(‘&first=‘)) but shorter, much shorter, run by injected HTML. Anyway, looking for a payload that runs eval(uri) simplifies the debug because the max length limit won’t be a problem, so let’s start from there.
Working on the solution
The first payloads that popped up in mind, to inject HTML and run JS were:
- <img src=x onerror=eval(uri)> exceeds the max length of 24, damn
- <svg onload=eval(uri)> does not work in Firefox, damn!
- <iframe onload=eval(uri)> exceeds the max length of 24, for one char only: DAMN!!
Ok, after some struggle, a good candidate as starting payload is <style onload=eval(uri)> so the URL looks like this now:
Follow up problem: the call eval(uri) is resolved in eval(‘https://challenge-0222.intigriti.io/challenge/xss.html?q=<style onload=eval(uri)>’) . Because uri starts with “https://“, eval parses that as a JS label followed by a comment. With colors gets clearer, most of the URL is evaluated as a comment (in green) and the protocol is evaluated as a label (in violet):
On the browser console is triggered the error “unexpected end of input”: a label must be followed by a statement, which is the missing line 2.
The only way to end the single line comment is to go to the new line, so an encoded new line is added to the URL, as %0A:
Problem: the payload <style onload=eval(uri)>%0A is again longer than 24 chars. To bypass that, we change the URL as:
by adding &first=, the execution will strip out any text on the URL since &first= (included) and so %0A won’t be part of the injected HTML (see point 3. in the head of this section).
Before jumping to the final addition for the exploit, I have to say I didn’t think of %0A at first; I was thinking of the call eval(‘/*’+uri) with uri‘s value as follow (note the “&first=*/”):
which would have been interpreted as (the running code is highlighted, in green is the comment):
I like that approach but of course, the max length of 24 chars didn’t allow it. So %0A (as seen in the paragraph before) felt like the only viable option for the final solution.
Closed the digression that I hope was a nice insight, the last URL was missing only to execute the code from requirements; to refresh the memory, this is the last URL:
on the browser console is still listed the error “unexpected end of input” because eval still does not find a statement following the label “https:”. The code alert(document.domain) from the requirements will serve as a statement, so the URL becomes:
which is also a working solution for the challenge and the one I have submitted.
For clarity, the URL from above gets interpreted by eval(uri) as the two lines that follow (the colors are violet for the label, green for the comment, and black for the label’ statement):
the %0A is translated in a new line, that’s why the alert ends up on line 2.
With that we reached the end: I hope you enjoyed the post and found it insightful,
Thank you for reading
PS this write-up was shared from Intigriti’s official account on Twitter here: https://twitter.com/intigriti/status/1493209686681735168
feeling proud ❤