Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Report I wrote for the Intigriti DOM-Based XSS Challenge [https://challenge.intigriti.io/]

Summary

The endpoint https://challenge.intigriti.io/ is vulnerable to a DOM-based XSS which allows the attacker to execute arbitrary JavaScript in the context of the page's top-level window.

Consequences

Since the attacker can execute arbitrary JavaScript in the context of the page, he would be able to steal the user's credentials, or any other secrets contained in the webpage, and therefore hijack his session. He would also be able to redress the UI in order to deceive the user further.

POC

The solution

https://challenge.intigriti.io/#data:text/html;alert(document.domain);//;base64,PHNjcmlwdD5zZXRUaW1lb3V0KCgpID0+IHsgcGFyZW50LnBvc3RNZXNzYWdlKHt0ZXh0OiAxLCBodG1sOiAxfSwgIioiKSB9LCAxMDAwKTwvc2NyaXB0Pgo=

Explanation

The page https://challenge.intigriti.io/ parses the fragment part of its URL, replacing the strings <, > and script with forbidden. It then tries validating it as a URL by passing it to new URL(...).

If the above call succeeds, the page then creates an iframe with its src set to the fragment and appends the iframe to the document. Finally, it sets up a message event listener on the page's window object with the function executeCtx as its handler.

In the POC, the string-based filtering is easily circumvented by using a data URI to mask the fact that it contains HTML containing a <script> tag. The initial field of the data URI specifies that the payload is of mediatype text/html so that the browser interprets it as such. Ignoring for a moment the next two fields (because the browser does too), the last one specifies a base64 encoded string containing (a part of) our payload.

Decoded, the payload reads:

<script>setTimeout(() => { parent.postMessage({text: 1, html: 1}, "*") }, 1000)</script>

This code will be executed once the iframe is loaded, making the iframe send a message containing the object {text: 1, html: 1} after 1 second. The intended target of the message is left unspecified with *.

After a second, the message is received by the message event handler on the parent window of the iframe. Due to the Object.assign(...) call, we are able to set the text and html variables to numeric values. Now the url variable containing our initial fragment is eval-ed, this time being interpreted as JavaScript code.

eval ignores anything up to and including the colon, so data: is skipped. It then tries dividing text with html, which now succeeds because we previously set them to 1. Then the first ignored field is run, containing our actual payload. This executes alert(document.domain) in the context of the top-level page. Finally, the second ignored field contains // to make the rest of the data URI a JavaScript comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment