0324: Contact Form
Writeup for the Intigriti March 2024 challenge 💥
Last updated
Writeup for the Intigriti March 2024 challenge 💥
Last updated
XSS, Prototype Poisoning, Unicode Case Mapping Collision
Find a way to execute an alert(1337) utilising XSS on the challenge page and win Intigriti swag.
Reviewing index.html
, we have a basic contact info form. If we check the source, there's some JS. Starting with a runCmdToken
function.
The eval
function will execute the cmd
, but there are a few steps/conditions. The user token
must be 32 characters long, and then a string will be compiled. The hash
and cmd
are sliced from the string, and the resulting command will be evaluated.
Before reviewing the rest of the code, let's see what this looks like in a debugger. I set a breakpoint at line 49, entered the token cat
, and clicked "input" and then "info."
str
: 5eca9bd3eb07c006cd43ae48dfde7fd3alert(hash)
hash
: d077f244def8a70e5ea758bd8352fcd8
cmd
: alert(hash)
An alert pops with the hash! Wow, look at that.. We're already halfway there and haven't done anything yet 😆 All we need to do is replace that hash with 1337
. How might we go about it?
We can't modify the token value directly because the following function is hashing our input.
We've tested the "set token" form, but what's the "contact info" form doing?
The nameParam
, contactParam
, and valueParam
are passed to the handleInputName
function. Let's say we provide cat
, email
, and crypto@cat
, the result will look like this:
There's a prototype poisoning vulnerability here since user-controllable properties are being merged into an existing object, without first sanitising the keys. The vulnerability is similar to prototype pollution, but there is a crucial distinction.
Prototype poisoning is distinguished from pollution by the limitation that the parent object prototypes are immutable. The attacker can only affect the input object and children that inherit its prototype. Therefore the availability, attack methodology and impact are different. Prototype inheritance is an unavoidable JavaScript functionality.
If we now provide __proto__
, token
and 1337
as our "set contact info" values, the resulting user
object will be poisoned.
Checking the console, user.token
is now set to 1337
(Note that token
is undefined since this is poisoning, not pollution)! The alert doesn't pop, though. Rechecking the source, we see the condition for runCmdToken
.
So let's try https://challenge-0324.intigriti.io/challenge/index.html?setName=proto&setContact=token&setValue=1337&runTokenInfo=1
It still doesn't pop, but remember this other condition?
Let's try a 32-character token: https://challenge-0324.intigriti.io/challenge/index.html?setName=proto&setContact=token&setValue=13371337133713371337133713371337&runTokenInfo=1
The alert pops with 13371337133713371337133713371337
. Many players opted to use null chars, spaces, tabs, etc to make it look like the correct payload, but this was rejected as an invalid solution - we need ^1337$
😁
There is a problem with the use of toLowerCase()
in the following line.
The second example from this article describes the issue.
Some Unicode characters are vulnerable to Case Mapping Collisions, when two different characters are uppercased or lowercased into the same character
They provide a simple PoC that you can test for yourself in the browser console.
The PoC is a good starting point, but notice it is for toUpperCase()
; our challenge uses toLowerCase()
.
So, we need to find some other Unicode character for our case mapping collision attack, e.g.
Notice that if we convert İ
to hex, it is 2 bytes.
Whereas, if we convert i
to hex, it is 1 byte.
Therefore, if we supply (16 * İ) + 16 char payload
, it will pass the initial length checks since it's 32 characters.
However, once it's converted to lowercase, it will become a 48-byte string: (32 * i) + 16 char payload
.
The next lines will separate the values using a slice
operation. The iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii
will land in the hash
variable, and our 16-byte XSS payload will end up in the cmd
variable.
However, our payload is only 11 characters.
Since 16
+ 11
= 27
, it's not the 32
character token we need 🤔
No worries! We can add some padding by inserting a comment at the end 😏
Putting everything together, we have the following payload https://challenge-0324.intigriti.io/challenge/index.html?setName=proto&setContact=token&setValue=İİİİİİİİİİİİİİİİalert(1337)//cat&runTokenInfo=1 which triggers our intended alert(1337)
😎
Some players used a script to find valid Unicode chars, e.g., here's one from ZePacifist
Note that there is only a single result.
The monthly challenge usually requires players to execute arbitrary JS, proved with alert(document.domain)
. The goal was set at alert(1337)
in this case due to the length restrictions on the payload. However, several players did find their way around this!
Here's an example from mysterican
The payload imports a script from nj.rs
, which triggers the alert.