This challenge takes a Python script, checks if it's safe to execute, then executes it. The safety check is done using Python's ast
module; it first converts the source code to an AST, then checks if it contails any node of type Import
, ImportFrom
, or Call
. If any of these nodes are found, then the script is considered insecure; otherwise, it's secure, and it gets executed. Essentially, we're not allowed to use import statements or function calls.
Luckily, there are ways we can get around both restrictions, by using the functionality of both of these statements without actually putting them in our source code. First, to get around import
statements, we can use the __import__
builtin function. Basically, the statement
import module
in Python is actually just syntax sugar for
module = __import__('module')
Therefore, we can actually import whatever we want without using an import statement.
To get function calls, we can use an advanced Python feature called decorators. Basically, decorators are annotations you can put on functions and classes to change things about them. For example, the standard library has a decorator that you can apply to a function to make it cache its outputs. Decorators use the following syntax:
@decorator
def func():
...
However, decorators are actually just syntax sugar for something much simpler. The above example is equivalent to:
def func():
...
func = decorator(func)
This means we can use decorators to perform function calls, just with some weirder syntax. Putting this together with our __import__
trick, we can now do all of the things the sandbox is supposed to prevent us from doing. The challenge tells us that the flag is in the file flag
in the current directory, so we want to call os.system('cat flag')
. This requires us to write the code:
def os_str(x): return 'os'
# This is equivalent to `os = __import__('os')`,
# which is equivalent to `import os`
@__import__
@os_str
def os(): pass
def cmd_str(x): return 'cat flag'
# This is equivalent to `ret = os.system('cat flag')`
@os.system
@cmd_str
def ret(): pass
Sending this code to the challenge gives us the flag.