Published on

Intigriti's March Challenge

Authors
  • avatar
    Name
    Amr Zaki
    Twitter

Challenge 0324

Introduction

Hello folks, this is a write-up for the March XSS challenge on intigriti. This is an XSS challenge with Prototype Pollution and Unicode Normalization. The challenge had 2 versions, in the easy one we had to execute exactly alert(1337), and in the hard version we had to execute arbitrary javascript. In this write-up, I will try to explain both solutions.

So, let's dive in.


Recon

The challenge consists of one endpoint and it is the home page.

image

The functionality of the page is simple, and pretty straightforward. In the source code there's an empty user object

var user = {};

and we can populate this object with Name, Content, Value, and Token fields using the Input button in both forms.

The value we insert inside of the Token input is hashed with the MD5 hashing algorithm using the CryptoJS function in core.js.

image

The info button in both forms just alerts the values we inserted.

image image

That's the whole functionality of the application, let's head to code analysis.


Code Analysis

In the source code, we have 2 files core.js and md5.js in the xss_files directory, I took the bait and went through both files because I thought the vulnerability was in them. I used goolge to search for CryptoJS exploits and found it vulnerable to CVE-2023-46233, I thought this was intended and spent some time reading about the CVE but nothing was related to our application functionality.

So let's skip these files and let's see the code in between the script tags.

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');
}

Starting from the very first line, we see the empty user object being created. After that we have 4 functions, let's break down each of them:

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

This function is called when we click input on the contact form and takes our name, contact, and value insert them in the user object.

Notice there's no sanitization being done to our input, so on first look it is vulnerable to Prototype Pollution.

  • runCmdName:
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)`);
}

This function is called when we click info on the contact form, and alert the inputs we inserted earlier. It first checks that there is at least 1 key in the user object other than "token", and then checks whether this key has a value or not. Last line constructs the alert dynamically with eval.

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

This function takes our input in the token form and hashes it with MD5, and then adds it to the user object.

  • runCmdToken:
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 function is responsable for the alert in the Token form, it constructs the alert() command dynamically with eval. The last 10 lines of code are just there to help us run our exploit using the URL.


Intended solution

As we saw earlier, the home page is vulnerable to Prototype Pollution in the handleInputName function. We need to use this vulnerability to be able to execute arbitrary javascript. First things first, let's prove the Prototype Pollution

image

When we send these values, the user object is constructed like this:

user['__proto__'] = {'z4ki' : 'polluted'}

This creates a property z4ki with value polluted. We have 2 possible ways to execute arbitrary javascript, using the runCmdToken function or using the runCmdName function. If we chose to use runCmdName, the function checks for another key in the user object, so we need to pollute the object twice, and even if we could, our input is put inside a string we can't break out of. That leaves us with the runCmdToken function approach.

Using developer tools we can see how our input is processed and what is the input to the eval function. So let's use the function as intended, put a breakpoint, and see the input to the eval function.

image

We can see the str variable consists of 3 parts, the token MD5 hash, the input to runCmdToken function which in our case is alert, and (hash) which refers to the hash variable. The hash variable is a sliced string from str 32 characters long, and the rest is the cmd variable value. The slicing is hardcoded as 32 characters, that's why our payload has to be exactly 32 characters.

Now we are certain the Prototype Pollution exists, let's try to manipulate other values in the user object. Without setting a token, we will try to add a token property with a random value of our own and see if it shows on the alert. The token value has to be 32 characters.

image

After we managed to bypass the hash function in the token and we can now add any 32 characters we want, it is time to move to the next line which converts our input to lowercase. After our input passes the length check, we want it to "expand" to more than 32 characters somehow so we can control the cmd variable after the slicing operation. Here comes the unicode normalization trick, since javascript accepts UTF-8 characters, there are certain UTF-8 characters that get normalized when converted to lowercase or uppercase format. This great resource lists almost every normalized unicode character. So let's search for characters that change when lowercased

image

We have 2 characters only, and since we need our inputs to increase in size, we can only use the first character İ because it will be converted to 2 characters. Let's verify this by sending 32 İ characters to the function and putting a breakpoint before we exit the function to see how are input is processed.

image image

As we can see in the second picture, user['token'] is indeed 32 characters, but after the lowercase operation it became 75 characters. If we removed alert(hash) which is 11 characters, our input becomes 64 characters which is 32 lowercased İ.

When we resume the program, we get the error unknown command, becuase the alert command is malformed. I fixed it as follows with this as the payload:

// this line is 32 characters.
İİİİİİİİİİİİİİİ;;alert(1337);//;

So cmd variable will be like this

image

Lastly let's construct the URL:

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;;alert(1337);//;&runTokenInfo=1

Opening this, the alert(1337) is executed and we managed to solve the easy version of the challenge.


Unintended solution

A day after I submitted the above solution, this message was sent on the intigriti's discord server.

image

I didn't know how one could execute arbitrary javascript because of the length restrictions, I barely managed to execute alert(1337) and got like 4 more characters to spare. So, I decided to try.

I started looking for tiny XSS payloads the first one is the tiniest, and it is 23 characters long. But we don't need the svg tag nor the onload event, we are only interested in eval(name). To be quite honest, I didn't understand the payload at first, so I did some searching on how does this actually work and I came across this link In short, if we can add eval(name) to the vulnerable website, then we can set the name property from another window or website, redirect to the vulnerable website and it will be inhereted by it. The first answer explains it very well and also provide us with an example on how to use that payload. So let's create a simple HTML page with the following script

<script>
window.name = 'alert(document.domain)';
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);/////&runTokenInfo=1';
</script>

The used payload is

İİİİİİİİİİİİİİİİeval(name);/////

Opening our created page, we get the alert and we manage to solve the hard version as well.

image

This was very fun to do and I learned a new thing while solving the hard version hope you learned something as well. See you next month ❤️