SECCON CTF 2022 Quals are over and I have to say that the tasks which I looked at were of pretty amazing quality. The task that I started with was called "find flag" and was authored by ptr-yudai. While it's a tiny warmup challenge, I found it extremely clever! So, here's a writeup.
#!/usr/bin/env python3.9
import os
FLAG = os.getenv("FLAG", "FAKECON{*** REDUCTED ***}").encode()
def check():
try:
filename = input("filename: ")
if open(filename, "rb").read(len(FLAG)) == FLAG:
return True
except FileNotFoundError:
print("[-] missing")
except IsADirectoryError:
print("[-] seems wrong")
except PermissionError:
print("[-] not mine")
except OSError:
print("[-] hurting my eyes")
except KeyboardInterrupt:
print("[-] gone")
return False
if __name__ == '__main__':
try:
check = check()
except:
print("[-] something went wrong")
exit(1)
finally:
if check:
print("[+] congrats!")
print(FLAG.decode())
When taking the task at the face value it looks like it's about finding a file containing the on the filesystem. This in all honesty was my initial thought as well. And due to reasons (a certain exercise I make some of people I'm mentoring do*) I've solved the task by accident before I fully understood what's going on. Let's look at the details.
* The exercise goes as follows: "having open(CONTROLLED) in Python, make it throw as many different exceptions as you can manage; do this on Windows and Linux separately". The idea there is to point out how misleading the documentation can be (it literally says "[if] the file cannot be opened, an OSError is raised"), push for a bit of creativity and a hacker's mindset, and show that dealing with files is complicated.
There are basically two pieces of code: the check function and the "main" part of the code in global space, which for simplicity I will call the main function.
The main function basically calls the check function and verifies whether it returns True – or rather something that evaluates as true; note the difference between if check: and if check is True. In case of any exception it prints out "something went wrong" and calls exit(1).
The check function tries to open a file – the player controls its name – then reads its content and compares it with the actual flag. If it matches, True is returned. If any of the handled exceptions happens or the flag does not match the read content, False is returned.
So, on face value, to get the flag one needs to discover where is it stored on the filesystem, be it intentionally or due to some inner workings of the operating system. For example, /proc/self/environ immediately comes to mind (as the flag is originally read from environment variables), however it won't work since the condition requires for the flag to be at the very beginning of the file read.
The thing is... that's not what the task is about at all.
Let me start by saying how I solved it: I passed a null-byte as the file name.
I encourage the reader to take a break and look at the code again knowing the solution. It is still not trivial to see why that would work! And that's how we reach the beauty of this challenge.
There are several things one has to notice (or know) in this case:
So this task ended up being a mix of open throwing a lot of weird exceptions, a conditional shadowing of a global/local name, and exit(1) not exiting before the finally-block is executed. I found it pretty amazing! Once again kudos to the task author - ptr-yudai!
P.S. Can you think of other ways to make open throw different unhandled exceptions? ;>