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 flag 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:
- The first one is pretty obvious and I've already hinted at it. Not all exceptions have been handled in the check function. Case in point being the null-byte which – as part of poison null-byte protection – raises a ValueError. This causes the exception to "walk up the call stack" to find an except-block which will handle it. In case of this task that's the catch-all except-block in the main function.
- Second is harder to spot and is commonly a subject of confusion for more junior programmers – shadowing. Note that there are two checks. There's the check function, but there's also the check local variable in the main function. If we look at the finally-block in the main function, we notice that check is used there as a condition to show the flag. However it's actually not clear whether it refers to the function or the local variable. To be more precise, there are two options. If no exception is thrown, then the check = check() line will call the check function and only then create the check local variable and initialize its value with whatever the function call returned. When an exception is raised inside the check function however, then the local variable is never created. Given this, the if check: code in the finally-block may refer to either one of these, depending on whether or not an uncaught exception was raised in the check function. And of course if the flag-printing condition evaluates the check function, it will come to the conclusion that it's True and show the flag.
- The third surprise comes with exit(1) not immediately exiting. I guess C/C++ programmers kinda know this from the exit vs _exit discrepancy, but an "exit" function might delay the exiting to do a few more things. In case of Python what exit, or sys.exit for that matter, do is throw a SystemExit exception. And given that it's an exception, all the normal rules of exception handling apply – including executing the finally-block – just in time to print out our flag.
By the way...
There are more blog posts you might like on my company's blog: https://hexarcana.ch/b/
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? ;>
Add a comment: