Web challenges are well-designed most of them are client-side issues, and we solved 4 out of 5 challenges.
"ALL" THE LITTLE THINGS
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.
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.
Now let us see the functionality
/settings -> To update name, profile picture with URL, theme, and interestingly there is a hidden parameter __debug__ which enables some kind of client-side debugging.
Below code, takes the JSON data from the form input and interestingly sets the value to window.name, this is crucial for the next steps.
/note -> add private or public notes and view them.
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.
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?
Let's test this on server, visit this https://pasteurize.web.ctfcompetition.com/a471a641-a5c3-4c84-a01f-f29c7eb3c768,.
We got, XSS now we just need to report with a payload which steals admin notes.
Back to "ALL THE LITTLE THINGS"
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_.=]
- Prototype pollution
When we enable debugging using __debug__ parameter, we can provide options like below, and they are saved on window.name to keep the values even after the refresh. window.name is parsed and assigned to User Object.
Provide below input and refresh the page, you can notice a pop up with [Object object]
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
- Cross-Origin window.name
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 window.name 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.
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.
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.
Create a iframe "x" with empty script nodes(<script></script>), you may have a doubt that it will throw CSP error no it wont because when we insert a script node with empty content it is considered as non parser inserted code and html parser wont execute this node and we won't get a csp error. You can read more about this in html5 specification here https://html.spec.whatwg.org/multipage/scripting.html
Using an iframe with first callback we read the nonce value from parent document and save it in "x" frame.
2nd callback to set the nonce for the empty script nodes we create in step 1.
After the CTF, Terjanq posted a fixed version of the challenge(https://fixedthings-vwatzbndzbawnsfs.web.ctfcompetition.com/), and we decided to solve the intended one.
You can check the changes made to the new version here https://www.diffchecker.com/BwidGBfk.
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 window.name 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.
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.
And redirect to the above note with all as a username.
https://msrkp.github.io/googlectf-2020/5.html (uh-oh its a private one)
So, we can now change the callback with any value we wanted.
We create a note as follows, which writes window.name value to document.
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.