Magpie CTF 2022
A jeopardy-style CTF hosted by the University of Calgary.
About
Magpie CTF was a jeopardy-style CTF held virtually on February 25th to the 27th. It featured challenges from various different categories such as web application, reverse engineering, OSINT, etc. It was a team based competition primarily made for students, with no entry fee.
The competition told a heist themed story where a fictional company (Mom & Pops Flag Shop), acquired many other flag companies across the nation and achieved a monopoly in the flag market. Our mission as the competitors was to:
- reclaim the flags that Mom & Pops Flag Shop acquired
- find out more who exactly “Mom” and “Pop” were
- and figure out how they managed to corner the market
This story would unravel with each challenge, giving more and more hints as to what the company has done; Elements like these are what made Magpie CTF stand out in my eyes.
As more and more competitors solved challenges, the point value for those challenges would start depreciating. This would also lower the value they held for the competitors that already solved the challenge, ultimately lowering the value of the challenge as more and more people solved it.
During this CTF, the organizers would stream a variety of heist movies such as: Ocean’s 11, Mission Impossible, Inside Man, Inception, etc; as well as status updates and SkipTheDishes gift card giveaways to lucky competitors.
Prizes
The prizes for this CTF were as follows:
Placement | Prize |
---|---|
1st Place | $1024 |
2nd Place | $512 |
3rd Place | $256 |
There were also an additional 3 prizes of $128 being handed out for the top 3 challenge write-ups.
Results
My team and I placed 16th by the end of the competition.
Even though our placement wasn’t anyting remarkable, we still walked away with a unique experience very few other CTFs had.
Challenges
Here were some of the challenges we solved that I thought were interesting.
Blame The Intern - Web
This challenge tasked us with escaping a Python application written with Flask. This application simply took a query we gave it and generated a fake flag.
We were also given the source code of the application, with the following snippets of interest:
# Handle app state
STATE = {
"success": False,
"id": 56645,
"username": "timtheintern",
"secret": "THISISASUPERSECRETKEY",
"flag": os.environ['FLAG']
}
...
# Handle POST requests to /submit
@app.route("/submit", methods=["POST"])
def submit():
result=request.form.to_dict(flat=False)
STATE['result'] = result
input_string = f"{result['try'][0]}"
count_string = f"{gen_useless_flag()}"
return render_template_string("<!DOCTYPE html>" \
"<html>" \
"<head>" \
"<title>Flag Licenser</title>" \
'<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">' \
"<head>" \
"<body>" \
"<!-- This is our default flask template -->" \
"<!-- Remove template string rendering or sanitize input before pushing to production. -->" \
"<div class='w-full lg:w-1/3 mx-auto my-8 bg-gray-300 lg:border-0 lg:rounded-xl p-4'>" \
"<div class='text-center'>" \
"<span class='text-4xl'>Results</span><br>" \
f"<span class='text-md'>Your key is: </span><br>" \
f"<span class='text-xl font-bold'>{input_string}</span><br>" \
"<span class='text-md'>The flag is: </span><br>" \
f"<span class='text-xl font-bold'>{count_string}</span><br>" \
"</div>" \
"<a class='button' href='/'>Try Again</a>" \
"</div>" \
"</body>" \
"</html>", state=STATE)# Handle POST requests to /submit
The parts of focus in this case are the STATE
dictionary defined at the top of the file, the line in the reponse containing our query, and the state
variable defined at the end of the response.
Looking at the variable defined at the end of the response, it simply assigned the dictionary defined at the start of the script. This dictionary held a key value pair containing the flag stored in an environment variable. To call this key, we used the following query:
{{state['flag']}}
This called the flag key in the state
dictionary and returned us the flag.
Tracking the CEO - OSINT
This challenge was broken down into a few parts, with the ulimate goal being to track down the CEO and find their real identity.
The first thing we did when tackling this challenge was look for any social media accounts that may be related to the target. We came up with a bunch of username combinations, one of which yielded many results.
As we went down the list and started to check the Instagram result, we noticed that the account was both validm, and had a bunch of interesting posts.
The first of which was an image of a building. We started to investigate this image since it might clue us in as to where the home city of Mae is.
Looking at this image, we can tell that the CEOs home city was somewhere in Ukraine after translating the text written on the building. At this point, we started taking guesses by picking and choosing the most populated cities.
Reading this challenge’s hint, the flag would be somewhere on the Wikipedia page of Mae’s home city.
After about half an hour of looking through Wikipedia edit history, we finally found the flag within the Lviv page edits.
Along with the flag, a Discord server link was also included in the edit.
Entering this server, the only access we had was the ability to send messages in a single channel that would get checked against a password. If the message was not the password, it would simply get deleted.
We now started looking back to our previous social media results to see what we can go off of, and we noticed a video that Mae had on their Instagram. Playing this video, it was a bunch of different beeps with a random order.
Looking at the description of the post, we found that the beeps were coming from an old flip phone, emulating a message being sent.
After doing some research, we found that these “beeps” were DTMF codes, which meant that we were able to decode each beep into the number that was pressed on the flip phone. Then, it was just a matter of counting the amount of times a number was pressed to derive a letter.
To first decode each beep, we downloaded the video and converted it into a sound file.
Then, we downloaded dtmf-decoder off of Github, and fed the sound file to it.
Finally, we derived the letters corresponding with each number and the amount of times it was pressed.
This means that:
- 99 -> x
- 5 -> j
- 8 -> t
- 66 -> n
- 4 -> g
- 7 -> p
Entering xjtngp
into the Discord server gave us a role that allowed us to see other channels in the server, and in turn revealed the next flag.
Scattered Letters - Networks
This challenge involved a custom made mail server and web app. Our goal was to retrieve the flag hidden in another users email.
To complete this challenge, we simply explored the web app by first registering an account and then checking our emails.
Being able to check our emails, we now have the email address of the IT department as a target for privilege escalation.
To perform privilege escalation, we analysed the login flow of the email service and began tampering with the headers. One of the requests that was sent during the login flow was a POST request for an endpoint that lists emails containing the user ID of the end user. We simply intercepted this request during the login and changed it to the email address of the IT department we found earlier.
As a result, we were able to see the emails belonging to the IT user.
The request that would get sent whenever a user would try to view an email had a similar flaw, and so we exploited that flaw to check the IT user’s emails.
While there was nothing of interest in the emails (yes, we tried those passwords), we were given a new email address to escalate to, the administrator.
Using the same method, we were able to find the flag in the administrator’s emails.
Mom & Pops HQ - Reverse Engineering
This challenge would give us an ELF binary that we would need to reverse engineer to figure out what the flag is.
Running the binary, it would prompt us asking for a key code. We assumed that entering the correct key code would return the flag.
After trying a bunch of simple commands to try and get the flag (strings
), we decided to import the file into Ghidra and tried to see where the key code comparison was made.
The farthest we got before the next step was a few functions that checked the input for special conditions.
One of the functions would check if the input was 8 characters long (line 16):
Then it would call a function with our input passed into it to check if each character matches the keycode.
Each function would just return a single character.
At this point, my teammate relayed to me all the possible characters and the length of keycode, so I decided to make a Python script that would try all the possible combinations of the given letters to save time. The following script took a few minutes to fully complete:
#!/usr/bin/python3
import subprocess
from itertools import permutations
values = permutations(['O','M','I','C','R','O','N','P'])
for i in list(values):
print()
value = "".join(i)
subprocess.run("echo 'Now trying: {value}'".format(value=value), shell=True)
subprocess.run("echo '{value}' | ./CONFIDENTIAL_BLUEPRINTS".format(value=value), shell=True)
The keycode ended up being OMNICORP
(go figure…), and the flag ended up being encoded as ASCII art.
Alternative Solution
Alternatively, the solution could have been discovered by manually sorting each character position with the characters that each function returned.
Mission Impossible - Miscellaneous
This challenge gave us a Python script that was running a web app. Our goal was to “deactivate the lasers”, which referred to a YouTube livestream that would play a bunch of lasers guarding a flag.
Presumably, we needed to somehow break the web app in order to deactivate the lasers. One of the functions defined in the source code was named shutdown
, which we assumed to be the function we needed to successfully call in order to shutdown the lasers.
@app.route('/api/v1/security-controls/shutdown', methods=['POST'])
def shutdown():
if 'X-API-Key' in request.headers and request.headers['X-API-Key'] == CONFIG['API_KEY']:
request_data = request.get_json()
if request_data and 'lasers' in request_data:
lasers = request_data['lasers']
return laser_control.shutdown_lasers(lasers, request)
else:
return "Invalid JSON body data!\n"
return "You are not authorized to turn off the lasers!\n"
We were not given any API keys that we could have used to call this request and pass the authorization control. However, we noticed that at the top of the script, an API key variable was declared.
CONFIG = {
'API_KEY' : ENV_VALUES['API_KEY']
}
Our goal now became to somehow call this variable; It was declared globally, so it was possible to call it within a function.
We then scrolled further down the code and read a function named format_employee
# !!! EXPERIMENTAL !!!
#
# This API endpoint is functional but it has not been audited by our security team.
# While it is functional, we can not guarantee that there are no vulnerabilities.
@app.route('/api/v1/employees/format', methods=['GET'])
def format_employee():
if 'template' in request.args:
template = request.args['template']
return template.format(person=employees[0])
else:
return "Error: No template parameter provided. Please specify an template.\n"
Apart from the obvious comment, we knew that this was the function we needed to attack due to the use of .format
on the template
variable. This variable would contain our user input when calling the endpoint.
After providing the parameter template
, this script would grab the first item from the list of employees. This list of employees would simply contain objects with attributes containing the employee name, ID, and position.
class Employees:
def __init__(self, name, id, position):
self.name = name
self.id = id
self.position = position
def serialize(self):
return {
'name' : self.name,
'id' : self.id,
'position' : self.position
}
def get_id(self):
return self.id
employees = [
Employees('Ma', 0, 'CEO'),
Employees('Pa', 1, 'CEO'),
Employees('Jim', 2, 'Marketing'),
Employees('Diane', 3, 'Clerk')
]
This would only work as expected if we passed {person}
as the parameter value, since it formats that part of the string to be the list item.
Otherwise, it would simply reflect the given value.
To break this, we simply called the object with the __init__.__globals__
attribute.
Now that we have the API key, we proceeded to call the shutdown
function we saw earlier.
We ran into a few hiccups along the way, such as not being sure what to enter into the json body.
But eventually, we found a function api_docs
, which revealed the laser naming schema.
@app.route('/api/docs', methods=['GET'])
def api_docs():
return render_template('api-docs.html')
The last control we had to bypass was a rate limiter that would only allow us to shut off two lasers every hour.
To bypass this, we simply passed a list of lasers similar to the docs, but with all 4 lasers instead of only 2.
Crack the Safe - Miscellaneous
This challenge had us navigating to an oddly configured web server that would return a response every time we would enter a single character into the prompt.
Looking at the HTML source code of the webpage, we spotted something immediately out of the ordinary.
Line 35 contained a “hidden” image element. We ended up downloading and analyzing it.
Using binwalk
, we were able to find and extract some hidden files within the image.
One of the hidden files we found was some PHP source code.
<?php
$fp = fopen('./flag.zip', "rb");
$binary = fread($fp, filesize('flag.zip'));
$target = base64_encode($binary);
if (isset($_GET['userinput']) && substr($target, 0, strlen($_GET['userinput'])) == $_GET['userinput']) {
echo "<input type='text' id='userinput' name='userinput' oninput='doSubmit()' value='".$_GET['userinput']."' autofocus /><br>";
if ($target == $_GET['userinput']) {
echo "<b id='useroutput' class='someClass' style='text-align: center; color: #00FF00;'>" . $_GET['userinput'] . "</b>";
} else {
echo "<b id='useroutput' class='someClass' style='text-align: center; color: #FF0000;'>" . $_GET['userinput'] . "</b>";
}
} elseif (strlen($target) < strlen($_GET['userinput'])) {
echo "<input type='text' id='userinput' name='userinput' oninput='doSubmit()' value='".$_GET['userinput']."' autofocus /><br>";
echo "<b id='useroutput' class='someClass' style='text-align: center; color: #00FF00;'>" . $target . "</b>";
} else {
echo "<input type='text' id='userinput' name='userinput' oninput='doSubmit()' value='' autofocus />";
}
After realizing that the web app doesn’t actually do anything (entering the correct input just makes it turn green), we explored the flag.zip
file referenced at the top of the file. We did this by simply adding flag.zip
to the URI in order to download the file. Once downloaded, we tried to unzip it, but it was encrypted.
To crack it, we first extracted the hash from the file, and then ran john
against it with the rockyou wordlist.
Conclusion
Even though we did not have as much time as we wanted to participate in this CTF, it provided an interesting story, as well as some awesome challenges to keep us on our toes. Definitely one of the better CTFs we’ve come across.