Today we will be solving Intigriti’s August XSS challenge, which is based on an XSS cookbook. This cookbook has multiple XSS payloads listed together with the steps which needed to be performed. Somewhere in this application, there should be an XSS vulnerability.
If we take a look at the source code we can see that 3 scripts are being included, the main script with all the functionality and a jquery-deparam
library together with google analytics.
When the page gets loaded, the handleLoad
function will be called, which first gets the username from a cookie. The readCookie
function grabs the first cookie with the specified name and returns it.
// This thing is called after the page loaded or something. Not too sure...
const handleLoad = () => {
let username = readCookie('username');
if (!username) {
document.cookie = `username=unknownUser${Math.floor(Math.random() * (1000 + 1))};path=/`;
}
let recipe = deparam(atob(new URL(location.href).searchParams.get('recipe')));
ga('create', 'ga_r33l', 'auto');
welcomeUser(readCookie('username'));
generateRecipeText(recipe);
console.log(recipe)
}
If no username is present, it will create a random username.
On the next line, the recipe is begin loaded from the recipe argument. First, it is being base64 decoded, after which it will be passed to the deparam
library.
After loading the recipe it will initialize the google analytics tracking.
On the next line the current user will be greeted with the welcomeUser
function. This function adds the username insafely into the dom, but because this value is coming from a cookie, it should only be possible to perform self-XSS.
// As we are a professional company offering XSS recipes, we want to create a nice user experience where the user can have a cool name that is shown on the screen
// Our advisors say that it should be editable through the webinterface but I think our users are smart enough to just edit it in the cookies.
// This way no XSS will ever be possible because you cannot change the cookie unless you do it yourself!
function welcomeUser(username) {
let welcomeMessage = document.querySelector("#welcome");
welcomeMessage.innerHTML = `Welcome ${username}`;
}
Finally the recipe text will be generated and for the convenience it also writes the recipe object to the console.
In the generation of the recipe text, all values are setted by the innerText
property, which should be safe to XSS.
At Snyk we can see that the jquery-deparam library has a prototype pollution vulnerability.
BlackFan has created a nice overview of of gadgets in other javascript libarires, which can be abused with a parameter pollution vulnerability. Convinently it has a payload for Google Analytics: ?__proto__[cookieName]=COOKIE%3DInjection%3B
.
If we add the following parameter (&__proto__[cookieName]=username%3DCyberSpark%3B
) to our base64 encoded recipe
parameter, it should show Welcome CyberSpark
on the page. However this doesn’t happen. If we take a look at the cookie editor, we can see that the cookie is set:
Because Google Analytics is setting the cookie with a different domain, it won’t be overwritten. Google Analytics also has the cookieDomain
parameter, which can be used to set the domain for the cookie. However, this is not possible due to the usage of the 'auto'
cookie domain in the initialization. So we need to find another way to get the application to prefer our cookie instead of the legit one.
If we take a look at the RFC for cookies, it shows the following about cookie order:
1. The user agent SHOULD sort the cookie-list in the following
order:
* Cookies with longer paths are listed before cookies with
shorter paths.
* Among cookies that have equal-length path fields, cookies with
earlier creation-times are listed before cookies with later
creation-times.
NOTE: Not all user agents sort the cookie-list in this order, but
this order reflects common practice when this document was
written, and, historically, there have been servers that
(erroneously) depended on this order.
If we can create a cookie with a longer path than the original cookie (which is de default /
), our cookie gets placed first. Thus the readCookie
function will use our cookie.
digging trough the Analytics code, I found the cookiePath
parameter, which can be used to set the path for cookies. With the parameter pollution vulnerability, it should be possible to change this parameter. The XSS challenge is hosted on the /challenge/cooking.html
page, so if we choose this path it will work on the challenge and it will be longer than the default path. If we add the following snippet to the base64 encoded recipe it should create our preferred cookie: &__proto__[cookiePath]=/challenge/cooking.html
Now we should change our username to an XSS payload to obtain the Cross-Site-Scripting. For these kinds of injections, a <img src=x onerror=alert(document.domain)>
should work great. With the correct URL encoding on some spots, the full payload can look like this:
1title=Cyberspark%27s%20%20XSS&ingredients%5B%5D=A%20vulnerability%20in%20the%20deperam%20library&ingredients%5B%5D=A%20Google%20Analytics%20gadget&ingredients%5B%5D=An%20useful%20username&payload=%26%5F%5Fproto%5F%5F%5BcookieName%5D%3Dusername%3D%3Cimg%20src%3Dx%20onerror%3Dalert%28document%2Edomain%29%3E%3B%26%5F%5Fproto%5F%5F%5BcookiePath%5D%3D%2Fchallenge%2Fcooking%2Ehtml&steps%5B%5D=Find%20target&steps%5B%5D=Inject&steps%5B%5D=Enjoy&__proto__%5BcookieName%5D=username%3D%3Cimg%20src%3Dx%20onerror%3Dalert(document.domain)%3E%3B&__proto__%5BcookiePath%5D=/challenge/cooking.html
If we base64 encode this payload and add it to the recipe parameter, we should get code execution in the browser:
Click here to see the payload for yourself
Thanks Intigriti and WHOISbinit for this months challenge!