Skip to content

Instantly share code, notes, and snippets.

@nerder
Last active June 25, 2018 22:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nerder/03e5e0e4fd1baa3312b9d2e384b99885 to your computer and use it in GitHub Desktop.
Save nerder/03e5e0e4fd1baa3312b9d2e384b99885 to your computer and use it in GitHub Desktop.

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.

Cat Chat [Google CTF (Quals) 2018]

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.

Second part

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:

  1. /report
  2. 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.

Considerations

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.

Idea

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.

Third Part

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.

Bonus

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.

@jbdrvl
Copy link

jbdrvl commented Jun 24, 2018

In the source code:

`/secret asdfg` - Sets the admin password to be sent to the server with each command for authentication. It's enough to set it once a year, so no need to issue a /secret command every time you open a chat room.

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.

@jbdrvl
Copy link

jbdrvl commented Jun 24, 2018

What if the admin initiates the flag cookie when we initiate a room?

In that case, the admin does send the /secret message with its password at the beginning. So we could attack directly with nerder's idea..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment