Overview
This challenge gives us access to a Python shell where we can enter Python code and execute it, but the catch is that some restrictions are in place. This type of challenge is commonly referred to as a “pyjail”.
Solution
Typically, the target of a pyjail challenge is to figure out a way to read the flag. The flag can be stored in various locations, but in this challenge it’s located in the file flag.txt
, so that’s what we need to read the contents of.
Let’s take a look at the script and break down how it works.
#!/usr/local/bin/python
def better_eval(untrusted_code):
blocked_terms = ["flag", "+", "import", "os", "eval", "exec"]
for term in blocked_terms:
if term in untrusted_code:
print(f"The term {term} is filtered!")
return
try:
# Execute the user input in the restricted environment without globals or locals
print(eval(untrusted_code))
except Exception as e:
print(f"Error: {e}")
while True:
untrusted_code = input("Enter your python code> ")
better_eval(untrusted_code)
At the bottom, we see that our input is stored in the variable untrusted_code
, which is then passed to better_eval()
. This function then checks if the strings “flag”, “+”, “import”, “os”, “eval”, or “exec” are in the input we sent, and if they are, the function simply returns. If our code is “clean”, the function will instead run our code using eval()
and print the output.
The eval
function works by evaluating an expression. An expression is a piece of code that produces a “value”, which is really just any Python object in this case. Expressions can include function calls, which is important for us. eval()
doesn’t process statements, which can be one to multiple lines of code that perform an action and don’t have to return a value, such as assigning a variable (x = 3
) or execuing a loop.
With that in mind, let’s craft an expression that’ll return the the flag as its “value”. First, let’s check what functions we have access to. Python has functions that can be called without importing anything, which we’ll make use of to circumvent the fact that “import” is a blocked word. To check if we have access to these built-in functions, we can run the expression '__builtins__' in globals()
:
Enter your python code> '__builtins__' in globals()
True
The expression returned True
, signifying that the __builtins__
module is part of the global variables (obtained from running globals()
), which means that built-in functions are available to us.
If we look through the list of built-in functions in Python’s documentation, we see the function open()
, which takes in a filename (or path) and a string signifying the mode of operation. open()
then returns a file object that has access to the methods read()
and write()
. In our case, we’ll want to open “flag.txt” and read the contents, so our expression will look something like this:
open('flag.txt', 'rb').read()
But wait! The word “flag” is blocked. We can try splitting the string up to get around this, with the easiest way being the use of the “+” operator. This would give us an expression like "fl" + "ag.txt"
, but unfortunately, the “+” character is also blocked.
Enter the join()
method. When called on a string, join()
takes in a series of strings, puts all the strings together using the string join()
was called on as a separator, then outputs the new string. For example, the expression ','.join(["a","b","c"])
would result in the string "a,b,c"
. If we use a blank string ''
as a separator, then there won’t be anything separating the strings we join together. If we were to split the string “flag.txt” up using join()
and the blank string, it would look something like this:
''.join(['f', 'lag.txt'])
Knowing that the above expression will evaluate to “flag.txt”, we can take it and put it in place of the “flag.txt” in our call to open()
:
open(''.join(['f','lag.txt']),'rb').read()
This forms our complete expression, and when passed to the script as input, outputs the flag:
Enter your python code> open(''.join(['f','lag.txt']),'rb').read()
b'MetaCTF{f1l73rs_d0_n0t_s3cur3_u}'