Google CTF - 2020 Writeup

This year Google CTF was a blast, I played with the team I use Bing along with po6ix, arang and dbms335 for web category.

Web challenges are well-designed most of them are client-side issues, and we solved 4 out of 5 challenges.



I left a little secret in a note, but it's private, private is safe .

Note: TJMikeūüé§ from Pasteurize is also logged into the page.

If you are wondering who is TJMike is, it is an admin bot which is used in Pasteurize challenge. 

As far as I know, there are two solutions for the challenge, during the CTF we solved with an unintended solution and after the end of the CTF terjanq posted the challenge with fixes, so we decided to solve the fixed one.

Skip to Unintended Solution

Skip to Intended Write-up

Skip to Pasteurize Solution

Unintended Solution

It is a typical CTF web application for XSS challenge, where user can add and view notes and report the notes to admin(TJMike) bot.


The goal of this challenge is to achieve XSS under strict CSP. The value of is used to assign new properties on a User object without any validation, which leads to prototype pollution. There is a JSONP resource which used to set the theme. We can modify the callback value of the JSONP resource using the aforementioned prototype pollution, which leads to CSP Bypass and gives us the ability to run arbitrary javascript.

First and foremost thing we check on an XSS challenge is CSP, so looking at the below policy we can say that there are only two ways to execute javascript on the page either by including nonce or script resource from the same origin.

Content-Security-Policy: default-src 'none';script-src 'self' 'nonce-63174b61355ca2c7';img-src https: http:;connect-src 'self';style-src 'self';base-uri 'none';form-action 'self';font-src

Now let us see the functionality 


Below code, takes the JSON data from the form input and interestingly sets the value to, this is crucial for the next steps. 


For public notes, we can notice that its using Pasteurize website.

When we view the note, we can notice the below code, it looks similar to the Pasteurize challenge but with added nonce. The code below sanitizes the note contents with DOMPurify and adds into the Document.


Before moving forward lets see the pasteurize solution.

Pasteurize Solution

We are given with a website where user can add, view and report which is similar to this challenge.

Fortunately, there is no CSP set on the  site.

If we notice the below code, it similar to above one but without nonce. It is also mentioned in the comments that source is available at /source endpoint.


I removed most of the code for brevity, we can see that bodyParser is used with extended option which allows us to send arrays and objects in the request body. Without any sanitization the data is stored in the database, when retrieving the notes it is JSON stringified which escapes most of the characters.


So, now we know that we can send array what if we send an array?

> JSON.stringify("a").slice(1,-1); JSON.stringify(["a"]).slice(1,-1);


> JSON.stringify("a").slice(1,-1); 


Let's test this on server, visit this,.

$ curl -X POST --data "content[]=;alert(1337)//"

Found. Redirecting to /a471a641-a5c3-4c84-a01f-f29c7eb3c768

$ curl


------   ----- -----------


        const note = "";alert(1337)//"";

        const note_id = "a471a641-a5c3-4c84-a01f-f29c7eb3c768";

        const note_el = document.getElementById('note-content');

        const note_url_el = document.getElementById('note-title');

        const clean = DOMPurify.sanitize(note);

        note_el.innerHTML = clean;

        note_url_el.href = `/${note_id}`;

        note_url_el.innerHTML = `${note_id}`;


---- --- -----

We got, XSS now we just need to report with a payload which steals admin notes.


What if we tried the same array trick on this application, obviously it is not working so now we need to check if there are any juicy stuff in javascript files. 

From now on, I am adding comments in the code to describe their purpose.




Allowed character for callback are /theme?cb=[A-Za-z0-9_.=]





When we enable debugging using __debug__ parameter, we can provide options like below, and they are saved on to keep the values even after the refresh. is parsed and assigned to User Object.

"showAll": true,
"keepDebug": true,
"verbose": true

Provide below input and refresh the page, you can notice a pop up with [Object object]

"showAll" : true,
"keepDebug" : true,
"verbose" : true,
        "__proto__" : { "theme" : {"cb" :  "alert"} }

What's happening? 

If you notice User class there is no setter on theme, but we can make the user object to return any property value we wanted by overriding prototype(__proto__)

Check the below image for how its happening

Ok, now we can change the callback of script resource using prototype pollution, but we as an attacker cannot perform this in victim's page, so how can we do it?

We know that is used for saving debug options in this application if we can change that value we can achieve prototype pollution. Well, we can do that check the below image.

So we set with debug info on our page and redirect it to challenge page. Check this link, and you will notice an alert.

Arbitrary XSS

Let's try to inject arbitrary html first.

We add html in and write it in the document body.

We can't just inject <img src=x onerror=alert(1) /> because of CSP, so we need to find a way to respect CSP and achieve XSS.

There  are many ways to solve this, during the CTF I came up with below idea.

Multiple Callbacks

We can include callback resource which is on the same page as many times we wanted, but when inserted with innerHTML script tag wont execute so for that we use iframes.





And finally we have the solution, to read the flag we just have to make TJMike to visit this page using Pasteurize challenge.

Intended Solution

After the CTF, Terjanq posted a fixed version of the challenge(, and we decided to solve the intended one.

You can check the changes made to the new version here

Only changes made in debug.js, a new class named Debug is created and looks like that we can't perform prototype pollution as we have done in unintended one. Because the assignment of is performed on the Debug object, not on the User object, which means we cannot change the value of callback after all. 


We can't change the properties of the User object, but we can change the properties of the Debug object using the same __proto__ trick.

In the above updated debug.js file, if user.debug.debugUser is true, user.toString() is changed to user.debug.toString(). You know that we can make user.debug.toString() to return any value we wanted, so this brings us to concentrate on make_user_object in user.js.

We can set user object to any property which is not already defined on document, because of is_undefined(document[user.toString()]) check.


Visit and check window.USERNAME, it will be s1r1us.

While I am trying DOM Clobbering to point document[USERNAME].theme.cb to a URL. Posix found that document.all is undefined and it has a collection of all elements in the document, I am shocked like wth is happening, that's it we got the solution.  

Now it all makes sense that why the challenge name is "ALL" THE Little Things. xD

All we need to do is point document.all.theme.cb to an URL and use the same technique used in unintended solution to get arbitrary XSS.

So, we create a private note with below HTML.

<a id=theme href="v"><a id=theme name=cb href="http://x/=alert"></a></a>

And redirect to the above note with all as a username. (uh-oh its a private one)

So, we can now change the callback with any value we wanted.

Arbitrary XSS

We create a note as follows, which writes value to document.

 <a id=theme href="v"><a id=theme name=cb href="http://x/"></a></a>

Final Payload

Oh wait, notes are private how can TJMike(bot or admin) visit our private notes. How can we make the TJMike log in to attacker account and his account at the same time?

Fortunately, there is a way to solve this problem by setting our session cookies only on our /note/id endpoint and make TJMike visit that page.

We create a note in Pasteurize with below content, and we report it to TJMike which sets document.cookie on our private note path on a subdomain and redirects TJMike to our payload page.


Tech Support has this same problem we have an XSS on subdomain and self XSS on another subdomain where the flag located, so we use this technique to set our cookies on self XSS page which has a payload to read flag and navigate admin to self XSS page.

Wow-what a ride, really enjoyed solving this and thanks to Terjanq for a formidable challenge.