UofT CTF 2024 - [Zero] - Jail
Zero
Difficulty: Medium
Category: Jail
Flag: uoftctf{zero_security_too_apparently_lmao}
In this challenge, we are given the following PyJail:
1 |
|
This code sanitizes our input by removing all builtins and enforcing a blacklist on alphanumeric characters and double underscores “__”. This severely limits our ability to execute code, but as a wise man once said…
As soon as I saw this challenge, I immediately remembered two very similar challenges I solved from BYUCTF 2023: one which removed builtins and another which blacklisted alphanumeric characters. As this challenge is essentially a combination of the two, my solution incorporates similar techniques.
Our ultimate goal is to read the flag
file whose name/path is visible in the Dockerfile. In the absence of any restrictions, we could do this with something like
1 |
|
However, here we have several restrictions which prevent this simple code injection. The first one we need to bypass is the removal of builtins, which are native Python functions (such as print()
) that are typically available by default. To recover these, we can exploit the dunder method hierarchy on Python objects like lists []
, tuples ()
, etc. In essence, we can traverse “up” and “down” the dunder method hierarchy to access all builtin methods which were removed in the sanitization. The following will do the same as (1) above, bypassing the builtins removal:
1 |
|
(For more information on how this works, see here. Note that the index 121
may vary by machine, so some tweaking/bruting may be required.)
We can improve on (2) above to bypass the __ blacklist by using Unicode character U+FF3F (_
) (found here). Amazingly, Python interprets this character as an underscore in code execution, yet it passes the blacklist check!
1 |
|
Now we need to bypass the alphanumeric character restriction, and this is where the challenge really begins. Buckle up, it’s about to get messy…
First let us start with replacing all alphabet characters abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
. Just like with the underscores, we can utilize Unicode alphabet characters for parts of our exploit. The Python interpreter will perceive these as normal alphabet chars, but since they are not standard ASCII, they will bypass the blacklist check. I used some Unicode gothic alphabet characters (found here) as suitable substitutes for most of the exploit above:
1 |
|
Notice that we cannot use these gothic characters for the flag
file, since flag
is spelled with standard ASCII alphabet characters, and using 𝖋𝖑𝖆𝖌
will attempt to open a file which doesn’t exist. We need Python code which will form the string flag
during execution without explicitly using those characters. To get around this, we can use the __doc__
dunder attribute to obtain documentation about various objects and index that string to get the character we want. We can then concatenate the characters together to get the word flag
! For example, to obtain the character g, we can use the following:
1 |
|
This will access the tuple documentation and get the 38th character (g):
1 |
|
We can update (4) above using this method for all 4 characters of flag
to get:
1 |
|
This will read the flag
file, while bypassing all alphabet, double underscore, and builtins restrictions! Don’t believe me? Let’s do a sanity check:
The last restriction we need to bypass is the one on digits 0123456789
. This is where my payload becomes monstrously long, and I apologize in advance for any mental anguish or distress I cause readers of this writeup. Turn back now if you have a serious heart condition or experience nauseua when subjected to unapologetically obnoxious one-liners of code.
The basic idea to replace digits (which two of my teammates, ahh and Matthias, helped me realize) is that in Python, True
/False
are interpreted as 1
/0
when used in mathematical expressions. Thus, we can replace all numeric values in (5) with True+True+True+...
for all integers > 0 and False
in the case of 0. To avoid using the strings True
and False
directly, we can substitute expressions which evaluate to True
/False
, such as (''=='')
(True) and (''!='')
(False). Thus, to form any integer, we can just add arbitrary amounts of (''=='')+(''=='')+...
together. While simple in concept, this substitution lengthens the payload considerably :)
1 |
|
That’s it! Now with it working locally, we just need to test it on the server. Remember that the index 121
for the get_data
function I’ve been using may be different on the remote machine, so we need to brute force it. Besides that, the payload is essentially the same.
Python Solution:
1 |
|
Thanks for reading!