RedpwnCTF - Albatross
This was an awesome PyJail challenge from RedpwnCTF - we're provided with this source code,
This combines a traditional PyJail escape (with draconian conditions) with code golfing - one of my favourite pastimes.
Let's break down what we've been given - the comment tells us that the flag is in flag.txt, so our ultimate goal is to construct a payload to allow us to read /flag.txt - this could directly open and read the file, or more indirectly we could obtain a shell, and use that shell to read the file.
First, our payload is filtered -
This prevents us from passing any payload containing any of the characters in the blacklist by replacing each instance of a banned character with nothing - as
we are not allowed to pass payloads containing any letters, quotes or spaces! Already, this would be very difficult - but there's more! the [:n]
slices our payload, taking only the first n characters - functioning as a length limit. This length is defined at the top -
This is the code golfing aspect of the challenge - with a starting character limit of 30, every hour the amount of characters allowed increases by 2 - an ingenious method to promote code golfing, as the limit stops increasing once a solve has been found - so solving it earlier makes it harder for anyone else to solve!
Finally, our payload is executed via eval - but with a twist. The eval function actually takes two optional arguments as well as a string to evaluate -eval(source, globals=None, locals=None, /)
. This is because Python actually keeps track of local and global variables by storing them in their own dictionaries - you can see this by running globals()
or locals()
in a Python interpreter session -
Most important of all the items in this dictionary is __builtins__
- this points to a module containing all of the built-in functions that Python provides for you when you first start writing a program. This includes standard functions like print()
, chr()
and open()
, but also apparent syntax constructs like import - since import x
is transformed into __import__("x")
in parsing.
So, to circle all the way back around, eval allows you to set what the values of these two dictionaries will be when the given code is executed - in our example, they have been hardcoded to be empty with the __builtins__
option specifically set to None. This means that we do not have access to any of Python's built-in functions!
In summary, with the restrictions levied above we must somehow provide Python code to read the file /flag.txt, but this code must not use any built-in functions, contain any letters, quotes or spaces, and must be within a very specific length limit.
This may seem impossible at first (as the creators of the challenge warned us in the code comment!) so let's try dealing with one restriction at a time - the easiest one to tackle first is the lack of built-in functions.
Having your code executed without __builtins__
is the traditional PyJail setup, and thus there are multitudinous resources available detailing how to bypass this restriction. To give a summary, in Python everything is an object, and there are special attributes common to practically every object - for example, __doc__
stores a docstring describing the object and __dict__
stores a dictionary of all the attributes of that object. The plan is to use Python's inheritance mechanisms to obtain access to a class with useful values in its globals (for example, __builtins__
or the system
function) - we may not have access to functions, but we can obtain a class (in this case, the Tuple class, but any class could theoretically work) like so -
From here, we can gain access to the object
class like so
As practically everything in Python is an object, this class is the base for practically every single object - so we can now use this class to obtain a huge list of objects currently loaded by Python by listing all of the subclasses that inherit from this class
In turn, the sys
module imports the os
module, and keeps a list of which modules it has imported in modules
- the os
module contains the system
function - capable of executing a string passed to it and dumping the result to stdout - this is my ultimate goal. Putting all of that together, in my example I can obtain access to the system function like this, and thus give myself a shell
This solves our first problem, but this payload needs to contain no quotes, letters and spaces, as well as be short enough to qualify to solve this challenge. First, we can improve the length of this slightly - we could get access to __builtins__
directly from the sys
module, as it has its own copy, but we can note that at the top of the provided code it does import string, os
- meaning that their code actually has os
already imported as a global, and some objects from os
are present in the overall object list. Some enumeration revealed some promising candidates - I chose os._wrap_close
- it was present in the overall object space, and as part of the os module it also had every single other function from os
present in its globals - this meant I could shorten my payload considerably like so
Next, I decided to remove the quotes. Earlier, I mentioned that most objects have a docstring present in __doc__
- these are a reliable source of characters, and can be accessed without quotes. After analysing some docstrings, I found a relatively short way to obtain the "sh" string using a long step with a slice in the list docstring -
But rather than try and construct "system", I resolved to instead turn it into an offset - since __globals__
is a dictionary, I couldn't access it using a position like an array. But, if I could transform that dictionary into an array I could use the position in the list to access my chosen object without any quotes necessary! I used the dict.values()
function - this takes a dictionary and returns an array of the values of each item in that dictionary. It was almost perfect - except
It doesn't return a plain array, but rather a specific object - luckily, the dict_values
object can be de-encapsulated using the python spread operator, *. The spread operator takes an iterable, and passes each of the items in the iterable to the outer function call as separate arguments - combining this with a list constructor [], I could quickly convert my dict_values
object into a list, ready for me to index it.
I can now write my payload as
This was just barely short enough to pass the length limit at the time when I did the challenge, clocking in at 100 chars out of my 109 allocated. Now only one problem remains - the letters. While I haven't used any strings, I've used letters to access the properties of various objects, and it doesn't seem like there's any way around this - in fact, I'm pretty sure there isn't a way to complete this without accessing object properties like this! So, how do I use letters in my payload, but still use none of the blacklisted characters? Note that the blacklisted characters are specifically the ASCII letters, from the normal character set - the ones I'm writing this writeup in now. Python, however, is very willing to deal with exotic and unusual Unicode dialects - in fact, it will normalise Unicode given to it in some scenarios - including in the interactive console, and in evals! For example, I can call print without using any ASCII letters by replacing them with characters from the Unicode Gothic character set (or indeed any character set which normalises to plain letters), like so
This is the final step necessary for my payload - putting all of this together I end up with
Sending this to the server results in a shell, with which I can read the flag! flag{SH*T_I_h0pe_ur_n0t_b1c...if_y0u@r3,_th1$_isn't_th3_fl@g}
I really enjoyed this challenge - it was a fresh take on a classic PyJail challenge, and provided the perfect opportunity to do a deep dive into some Python internals, as well as have a little code golfing fun.
Last updated
Was this helpful?