Intigriti XSS Challenge - August 2021 - A venture into prototype pollution

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:

  1. Call ReadCookie and check if it returns anything.
  2. If not, set the username value in the cookie to be "unknownUser" and a random integer.
  3. Set the recipe variable to be equal to the Base64-decoded value of the recipe parameter.
  4. Set Google Analytics (hmmmm).
  5. Call welcomeUser.
  6. Call generateRecipeText.
  7. 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 innerHTMLof 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:


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:


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:


So in our case, we must craft it like:


We should also encode the second = for clarity. So I put this at the start of the parameter payload.


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


and when encoded, it becomes


which we set recipe to be equal to.

The final POC URL is

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