30
Dec
2012

29C3 CTF – Web 42

https://29c3ctf.aachen.ccc.de/challenges/14/
Points 600
Description: http://94.45.252.236/ (https://29c3ctf.aachen.ccc.de/static/dl/web42.tar)
HINTS:

  • class str(str)
  • This is an rce challenge
  • type(settings.SECRET_KEY)
  • For the web42 challenge we are given an archive containing some python files which form a django site. Almost all the pages are in the form of .py source, except for the settings file, which is given in the form of a .pyc bytecode file.

    The settings file contains various settings which are very important to the functioning of django. In this case the most interesting thing is the SECRET_KEY value, which is used to securely sign cookie values. The application stores some data serialized using the python pickle module in a cookie which is signed using this key; if the attacker has the key then you can get arbitrary code execution using a well-known malicious pickle message.

    The easiest way to get this running is of course by installing the webapplication in a local django install and then extracting the secret value from that. This does not seem to work however. Since this is a 600 point challenge and the settings file is only given in binary form there is probably some tricky code in there which gives the SECRET_KEY value a different value on the challenge system than on our local system.

    Running this .pyc through some of the standard python tools such as uncompyle2 and the dis module, gives some errors, so we need to find another way to extract the information. Just entering a python shell and importing the module does not really help much; looking at the SECRET_KEY value gives a string which looks nice and random but does not actually work when we try to use it to sign a cookie.

    Some while later a hint was given to look at the type of the SECRET_KEY object. This seems strange, since this value is expected to be a simple string value. As it turns out, it is in fact a subclass of the string class which is defined in the settings module itself. Since the decompilers aren’t working we have to figure this out some other way.

    To avoid any complications which might happen if the settings.pyc modifies itself after loading we don’t use ‘import settings’, but instead load the marshelled module object manually. You can then explore the tree of code objects manually. We’re looking for unusual code objects, something to explain how exactly settings.pyc is able to detect the context it is running in and give back a different SECRET_KEY.

    This program explores the constant objects used by the code:

    import marshal
    import dis
    
    m = marshal.loads(open('settings.pyc').read()[8:])
    
    def explore(m,n = 0, seen = None):
    	if seen is None: seen = set()
    	seen.add(m)
    	for i,c in enumerate(m.co_consts):
    		print "%s %d %r" % ("\t" * n, i, c)
    		if "code" in repr(c):
    			if c in seen:
    				print "\t" * n + "SEEN"
    			else:
    				explore(c, n + 1)
    
    explore(m)
    

    snippet of the output:

    ....
                                     3 <code object bar at 0xe4ebb0, file "/home/luks/coding/python/29c3/Web42Download/Web42/settings.py", line 868>
                             4 <code object bar at 0xe4edb0, file "/home/luks/coding/python/29c3/Web42Download/Web42/settings.py", line 873>
                                     0 <code object rVKgRosvAl at 0xe4ecb0, file "/home/luks/coding/python/29c3/Web42Download/Web42/settings.py", line 875>
                                             0 None
                                     1 'bar'
                                     2 <code object bar at 0xe4ed30, file "/home/luks/coding/python/29c3/Web42Download/Web42/settings.py", line 879>
             9 <code object __radd__ at 0xe59a30, file "/home/luks/coding/python/29c3/Web42Download/Web42/settings.py", line 885>
                     0 None
                     1 <code object NqRgFWOeMM at 0xe4ef30, file "/home/luks/coding/python/29c3/Web42Download/Web42/settings.py", line 887>
                             0 None
                     2 'bar'
                     3 <code object bar at 0xe50030, file "/home/luks/coding/python/29c3/Web42Download/Web42/settings.py", line 891>
                     4 <code object LkLknGvuQX at 0xe501b0, file "/home/luks/coding/python/29c3/Web42Download/Web42/settings.py", line 894>
    .....
    

    This shows an interesting function object: __radd__. This is a method objects can declare to override the addition operator when the declaring class is on the right of the operator. Let’s dump the bytecode for this method.

    import marshal
    import dis
    
    m = marshal.loads(open('settings.pyc').read()[8:])
    
    for i,c in enumerate(m.co_consts):
    	if "code" in repr(c):
    		for c2 in c.co_consts:
    			if "radd" in repr(c2):
    				tmp = c2
    
    print
    print "DUMPING str.__radd__"
    dis.dis(tmp)
    print
    print "DUMPING GENERATOR EXPR CONST 13"
    dis.dis(tmp.co_consts[13])
    print
    print "DUMPING GENERATOR EXPR CONST 15"
    dis.dis(tmp.co_consts[15])
    

    After dumping you can see that two generator expressions are used by the method, so these are dumped as well here.

    DUMPING str.__radd__
    887           0 LOAD_CONST               1 (<code object NqRgFWOeMM at 0x2122e30, file "/home/luks/coding/python/29c3/Web42Download/Web42/settings.py", line 887>)
                  3 MAKE_FUNCTION            0
                  6 STORE_FAST               2 (NqRgFWOeMM)
    
    891           9 LOAD_CONST               2 ('bar')
                 12 LOAD_GLOBAL              0 (object)
                 15 BUILD_TUPLE              1
                 18 LOAD_CONST               3 (<code object bar at 0x2122eb0, file "/home/luks/coding/python/29c3/Web42Download/Web42/settings.py", line 891>)
                 21 MAKE_FUNCTION            0
                 24 CALL_FUNCTION            0
                 27 BUILD_CLASS         
                 28 STORE_FAST               3 (bar)
    
    894          31 LOAD_CONST               4 (<code object LkLknGvuQX at 0x21240b0, file "/home/luks/coding/python/29c3/Web42Download/Web42/settings.py", line 894>)
                 34 MAKE_FUNCTION            0
                 37 STORE_FAST               4 (LkLknGvuQX)
    
    905          40 LOAD_CONST               2 ('bar')
                 43 LOAD_GLOBAL              0 (object)
                 46 BUILD_TUPLE              1
                 49 LOAD_CONST               5 (<code object bar at 0x2124230, file "/home/luks/coding/python/29c3/Web42Download/Web42/settings.py", line 905>)
                 52 MAKE_FUNCTION            0
                 55 CALL_FUNCTION            0
                 58 BUILD_CLASS         
                 59 STORE_FAST               3 (bar)
    
    915          62 LOAD_CONST               6 (<code object HpnvwGiZlB at 0x21246b0, file "/home/luks/coding/python/29c3/Web42Download/Web42/settings.py", line 915>)
                 65 MAKE_FUNCTION            0
                 68 STORE_FAST               5 (HpnvwGiZlB)
    
    947          71 LOAD_CONST               2 ('bar')
                 74 LOAD_GLOBAL              0 (object)
                 77 BUILD_TUPLE              1
                 80 LOAD_CONST               7 (<code object bar at 0x2124b30, file "/home/luks/coding/python/29c3/Web42Download/Web42/settings.py", line 947>)
                 83 MAKE_FUNCTION            0
                 86 CALL_FUNCTION            0
                 89 BUILD_CLASS         
                 90 STORE_FAST               3 (bar)
    
    978          93 LOAD_CONST               8 (<code object HUfmsAtWFg at 0x2129930, file "/home/luks/coding/python/29c3/Web42Download/Web42/settings.py", line 978>)
                 96 MAKE_FUNCTION            0
                 99 STORE_FAST               6 (HUfmsAtWFg)
    
    1073         102 LOAD_CONST               2 ('bar')
                105 LOAD_GLOBAL              0 (object)
                108 BUILD_TUPLE              1
                111 LOAD_CONST               9 (<code object bar at 0x212c730, file "/home/luks/coding/python/29c3/Web42Download/Web42/settings.py", line 1073>)
                114 MAKE_FUNCTION            0
                117 CALL_FUNCTION            0
                120 BUILD_CLASS         
                121 STORE_FAST               3 (bar)
    
    1166         124 LOAD_CONST              10 (-1)
                127 LOAD_CONST               0 (None)
                130 IMPORT_NAME              1 (__builtin__)
                133 STORE_FAST               7 (__builtin__)
    
    1167         136 LOAD_GLOBAL              2 (issubclass)
                139 LOAD_FAST                1 (other)
                142 LOAD_ATTR                3 (__class__)
                145 LOAD_FAST                7 (__builtin__)
                148 LOAD_ATTR                4 (str)
                151 CALL_FUNCTION            2
                154 POP_JUMP_IF_FALSE      307
    
    1168         157 LOAD_FAST                1 (other)
                160 LOAD_CONST              11 ('django.contrib.sessions.backends.signed_cookiessigner')
                163 COMPARE_OP               2 (==)
                166 POP_JUMP_IF_FALSE      307
                169 LOAD_GLOBAL              5 (os)
                172 LOAD_ATTR                6 (path)
                175 LOAD_ATTR                7 (isfile)
                178 LOAD_CONST              12 ('')
                181 LOAD_ATTR                8 (join)
                184 LOAD_CONST              13 (<code object <genexpr> at 0x212c7b0, file "/home/luks/coding/python/29c3/Web42Download/Web42/settings.py", line 1168>)
                187 MAKE_FUNCTION            0
                190 LOAD_CONST              14 ('8czg8`vc')
                193 GET_ITER            
                194 CALL_FUNCTION            1
                197 CALL_FUNCTION            1
                200 CALL_FUNCTION            1
                203 POP_JUMP_IF_FALSE      307
                206 LOAD_CONST              12 ('')
                209 LOAD_ATTR                8 (join)
                212 LOAD_CONST              15 (<code object <genexpr> at 0x212c830, file "/home/luks/coding/python/29c3/Web42Download/Web42/settings.py", line 1168>)
                215 MAKE_FUNCTION            0
                218 LOAD_CONST              16 ('yEIAO^yOX\\OX\x04ZS')
                221 GET_ITER            
                222 CALL_FUNCTION            1
                225 CALL_FUNCTION            1
                228 BUILD_LIST               0
                231 LOAD_GLOBAL              9 (inspect)
                234 LOAD_ATTR               10 (stack)
                237 CALL_FUNCTION            0
                240 GET_ITER            
            >>  241 FOR_ITER                28 (to 272)
                244 STORE_FAST               8 (entry)
                247 LOAD_GLOBAL              5 (os)
                250 LOAD_ATTR                6 (path)
                253 LOAD_ATTR               11 (basename)
                256 LOAD_FAST                8 (entry)
                259 LOAD_CONST              17 (1)
                262 BINARY_SUBSCR       
                263 CALL_FUNCTION            1
                266 LIST_APPEND              2
                269 JUMP_ABSOLUTE          241
            >>  272 COMPARE_OP               7 (not in)
                275 POP_JUMP_IF_FALSE      307
    
    1169         278 LOAD_FAST                1 (other)
                281 LOAD_ATTR               12 (__add__)
                284 LOAD_FAST                0 (self)
                287 LOAD_CONST               0 (None)
                290 LOAD_CONST               0 (None)
                293 LOAD_CONST              10 (-1)
                296 BUILD_SLICE              3
                299 BINARY_SUBSCR       
                300 CALL_FUNCTION            1
                303 RETURN_VALUE        
                304 JUMP_FORWARD             0 (to 307)
    
    1170     >>  307 LOAD_FAST                1 (other)
                310 LOAD_ATTR               12 (__add__)
                313 LOAD_FAST                0 (self)
                316 CALL_FUNCTION            1
                319 RETURN_VALUE        
    
    DUMPING GENERATOR EXPR CONST 13
    1168           0 LOAD_FAST                0 (.0)
            >>    3 FOR_ITER                27 (to 33)
                  6 STORE_FAST               1 (c)
                  9 LOAD_GLOBAL              0 (chr)
                 12 LOAD_GLOBAL              1 (ord)
                 15 LOAD_FAST                1 (c)
                 18 CALL_FUNCTION            1
                 21 LOAD_CONST               0 (23)
                 24 BINARY_XOR          
                 25 CALL_FUNCTION            1
                 28 YIELD_VALUE         
                 29 POP_TOP             
                 30 JUMP_ABSOLUTE            3
            >>   33 LOAD_CONST               1 (None)
                 36 RETURN_VALUE        
    
    DUMPING GENERATOR EXPR CONST 15
    1168           0 LOAD_FAST                0 (.0)
            >>    3 FOR_ITER                27 (to 33)
                  6 STORE_FAST               1 (c)
                  9 LOAD_GLOBAL              0 (chr)
                 12 LOAD_GLOBAL              1 (ord)
                 15 LOAD_FAST                1 (c)
                 18 CALL_FUNCTION            1
                 21 LOAD_CONST               0 (42)
                 24 BINARY_XOR          
                 25 CALL_FUNCTION            1
                 28 YIELD_VALUE         
                 29 POP_TOP             
                 30 JUMP_ABSOLUTE            3
            >>   33 LOAD_CONST               1 (None)
                 36 RETURN_VALUE     
    

    One thing that immediately jumps out is this snippet:

    1168         157 LOAD_FAST                1 (other)
                160 LOAD_CONST              11 ('django.contrib.sessions.backends.signed_cookiessigner')
                163 COMPARE_OP               2 (==)
    

    ‘other’ is generally the name of the other operand in a an operator override method. this means that the other operand is being checked against a hardcoded string! And actually if you check what Django does, it actually always prepends precisely this string before using the SECRET_KEY value. So this answers the question why we can’t just ‘import settings; print settings.SECRET_KEY’, it is checking for Django!

    But when we try to run it in our own Django installation it still returns the same value as before, which does not work. Going back to the generator expressions, we can see they are simple xor decryption loops for two obfuscated strings in the main function. Decrypting these strings we get “/tmp/wat” and “SocketServer.py”. Analysing the program a bit more it turns out that it also checks whether SocketServer.py is anywhere on the call stack, and returns the bad value if it is. Additionally, the file “/tmp/wat” must be present, or the bad value is returned.

    So, if you make the file “/tmp/wat” and run this program…

    import settings
    
    print 'django.contrib.sessions.backends.signed_cookiessigner' + str(settings.SECRET_KEY)
    print 'django.contrib.sessions.backends.signed_cookiessigner' + settings.SECRET_KEY
    

    You indeed get two different results!

    django.contrib.sessions.backends.signed_cookiessignerelpcc8-d-d*+x2d04c^!(caf1@5xyl#%j73b3x*tvfrq3f+c4y
    django.contrib.sessions.backends.signed_cookiessignery4c+f3qrfvt*x3b37j%#lyx5@1fac(!^c40d2x+*d-d-8ccple
    

    In the second string the SECRET_KEY is reversed, as you can see. using the reversed SECRET_KEY value we can indeed sign our cookie and obtain a shell using a pickle exploit. Then it’s just a matter of reading the keyfile on the server πŸ™‚

    cd /home/flag
    cat flag.txt
    29c3_0761a7f0ecefde1841e7d88916754be3
    

    Flag: 29c3_0761a7f0ecefde1841e7d88916754be3

    Comments are closed.