DISCLAMER: I wasn't able to get the flag due to time shortage, but all the main ideas of the challange are here. This are basically the notes i take over the challange in order to figure out the solution of the puzzle. The approach i use in order to define a path toward the solution using the hints in the code that guides me in a specific direction.
The challange as far as i can tell consist in finding a Stored XSS and fish the admin to review the room to steel his cookies.
There are various hints that makes me think so:
headers: {
'Content-Security-Policy': [
'default-src \'self\'',
'style-src \'unsafe-inline\' \'self\'',
'script-src \'self\' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/',
'frame-src \'self\' https://www.google.com/recaptcha/',
].join('; ')
style-src
is set as unsafe-inline
// Admin helper function. Invoke this to automate banning people in a misbehaving room.
// Note: the admin will already have their secret set in the cookie (it's a cookie with long expiration),
// so no need to deal with /secret and such when joining a room.
function cleanupRoomFullOfBadPeople() {
send(`I've been notified that someone has brought up a forbidden topic. I will ruthlessly ban anyone who mentions d*gs going forward. Please just stop and start talking about cats for d*g's sake.`);
last = conversation.lastElementChild;
setInterval(function() {
var p;
while (p = last.nextElementSibling) {
last = p;
if (p.tagName != 'P' || p.children.length < 2) continue;
var name = p.children[0].innerText;
var msg = p.children[1].innerText;
if (msg.match(/dog/i)) {
send(`/ban ${name}`);
send(`As I said, d*g talk will not be tolerated.`);
}
}
}, 1000);
}
This function has beeing called when you run /report
. And i'm able to see the admin join the chat an run this helper function.
All the input are escaped using the funciton esc
that blacklist the common char used in common XSS payloads, but one input is not correctly sanitized.
display(`${esc(data.name)} was banned.<style>span[data-name^=${esc(data.name)}] { color: red; }</style>`);
in this one our payload is inside a css attribute selector, in which we don't need any of the blacklisted chars to craft a working payload. The one i'm using so far is this one:
admin]{color: red;}body{background-image: url(javascript:alert(1)}span[data-name^=admin
unluckily is not seems to work, or at least i'm not able to prove the opposite. In the past year was possible to craft a working payload that execute JS through CSS but not seems the case with modern browsers, even if i'm not aware of what actual admin's browser.
I think that this is the correct path, but probably it should be combined with something else (for instance i don't get what's /secret is used for) or eventually just find out a working payload.
The cookie of the admin should be the Flag.
I was finally able to understand how to fish the admin into my room and let him perform actions such as send messages and change is own cookies.
The way to reproduce is to simply enter a chat room and perform the following actions:
/report
- From a second session execute this request:
send?name=admin]{color: red;}body{background-image: url(https://cat-chat.web.ctfcompetition.com/room/5ab3252c-f7d9-4286-9a43-6395826a9766/send?name=admin&msg=%2Fsecret%20change_the_cookies);}span[data-name^=admin&msg=dog
This will make the admin ban your user with the malicious user name that allows us to make the admin perform the API calls and eventually change is own cookies and then close the session.
You can concatenate more then one background: url()
but because of CSP you can't load external resources with it.
Since you can't make the admin execute js, i don't actually know how to get the Cookies.
If we were able to make the admin call /secret
witout overwrite is own cookies and let him print the previous one we will have the flag in the data-secret
and we will be able to retrieve it writing a small bruteforcer using CSS attributes such as:
send?name=admin]{color: red;}body{background-image: url(https://cat-chat.web.ctfcompetition.com/room/5ab3252c-f7d9-4286-9a43-6395826a9766/send?name=admin&msg=%2Fsecret%20change_the_cookies);}span[data-secret^=CTF]{background-image: url(https://cat-chat.web.ctfcompetition.com/room/5ab3252c-f7d9-4286-9a43-6395826a9766/send?name=admin&msg=found); }span[data-name^=admin&msg=dog
In this case the admin will send found
in case the span[data-secret^=CTF] is correct (so data-secret starts with CTF
) and keep going like this until we have the complete flag.
Reading this line of code in the server i got an idea:
res.setHeader('Set-Cookie', 'flag=' + arg[1] + '; Path=/; Max-Age=31536000');
When we call /secret
this is the way in which the cookie are setted, if we where able to make this fail and or not overwrite the existing cookie but still returing { type:"secret" }
as response then the admin will print esc(cookie('flag'))
with the actual flag, and we will be albe to spoof the password with the technique explained before.
I was finally able to understand how to accomplish to call /secret
without change the actual cookie, and this was made using the Domain
attribute in Set-Cookie, so the paywload will be something like this:
/secret foo; Domain=www.something.com
So i have everything, but for some reason i was making some stupid mistake at the end that don't make my payloads works as expected, mainly because the shortage of time, since i got the Domain
idea just at 1h left of challange.
The challange was pretty complicated since many steps needs to be taken to solve it, but still enjoyable.
As an alternative solution to make the admin call /secret
was to rename the user /secret foo; Domain=
and the invoke the admin, type dog in order to get banned making infact the admin call /secret
since it will execute display(${esc(data.name). was banned.<style>span[data-name^=${esc(data.name)}] { color: red; }</style>);
adding the domain as expalin above don't screw up the real admin cookie, and in this way we where able to start a bruteforce using the exflitration technique explained above to retreive the flag.
In the source code:
Maybe if we can decrease the expiration date of the admin's
flag
cookie, it will force it to re-send a/secret
message to the server to renew its cookie. That way, we'll have it in the admin's HTML code, and we can apply the technique nerder talked about.