Created: 2014-11-08 00:49:29
Last modified: 2014-11-10 11:00:49
We were able to get some code running in a Daedalus browser. Unfortunately we can't quite get it to send us a cookie for its internal login page ourselves... But we can make it make requests that we can see, and it seems to be encrypting using ECB mode. See here for more details about what we can get. It's running at vuln2014.picoctf.com:65414. Can you get us the cookie?
In ECB mode, the same plaintext block appearing in two different places leads to the same ciphertext block appearing in both places. Can you figure out how to use this, and the encryption oracle that you have, to decrypt the cookies one byte at a time?
The server prepends the user input with 'GET /' and then appends a secret message. It encrypts this and sends it back to you. The object is to get the secrete message. The server encrypts in blocks of 16 bytes. Change the user input until it equals the response from the server. Guessing a whole block at a time is hard, but if you know the first 15 bytes and are guessing for the last 1 byte, it is doable.
The server concatenates a 5 byte string, your input (given as a hexidecimal string, where a pair of hexidecimal digits reperesent a character), and then the secret data. it encrypts all of that data, and sends it back (as a hexidecimal string) with a newline tacked on. I glean this from the following piece of code:
def oracle(s):
return AESCipher(key).encrypt(pkcs7_pad('GET /' + s.decode('hex') + secret_data))
...
data = self.request.recv(4096).strip('\n')
self.request.send(oracle(data).encode('hex') + '\n')
self.request.close()
Here is the code required to communicate with the server that abstracts away all of the monkey business away. It is often a good first step to abstract all of the irrelevant parts of a problem away:
# client side code
import socket
HOST = 'vuln2014.picoctf.com'
PORT = 65414
def send(string):
assert len(string) < 4096 / 2
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))
sock.recv(4096)
sock.sendall(string.encode('hex'))
resp = sock.recv(4096).strip('\n').decode('hex')
sock.close()
return resp
If we look at the encryption algorithm on the server, you will notice they are using PyCrypto. The PyCrypto documentation for AES says that the encryption works in blocks of 16 bytes. If two plaintext blocks are identical, then they will produce identical ciphertext blocks. This is called electronic codebook mode (ECB) of the AES cipher. You can read more about ECB and why it is insecure here. I hope to be able to attack this cipher by manipulating
(My send function takes care of encoding to hex and all that jazz.) I can find out the length of the secret. If I send an 8, 9, 10, or 11 long string, the length of the response is 80 bytes long. If I send a 12, 13, 14, or 15 long string, the length of the response is 76. Sending 11 bytes must make the message fit perfectly in 80 bytes. Sending 12 bytes must make it go one over 80, which needs another block (of 16 bytes), so the returned will end up being 96 characters long. 80 bytes - 5 bytes (for 'GET /') - 11 bytes (for user input) equals 64 bytes.
Here is the interesting part: if I send 11 bytes of filler text, I can finish
off the block started by GET /
. Then after that, I can send 15 bytes of
A
. Then I can send 15 more A
. Then the block will look like:
Secret starts here
V
G E T / A A A A A A A A A A | A A A A A A A A A A A A A A A A A A A A 1 | 2 3 4 ...
^ ^ ^
First block Second block Third block
where the | denotes a border of the blocks, and the number 1 represents the first character of the secret, 2 the second character, and so on. Once I have sent this request, I should store the second block of the result.
Now lets say I send another block. This time I send 15 A
followed by 15 more
A
, but after that put in a character, call it i
. that looks like this:
Secret starts here
V
G E T / A A A A A A A A A A | A A A A A A A A A A A A A A A A A A A A i | 1 2 3 ...
^ ^ ^
First block Second block Third block
If i
is the first character of the message, then the plaintext blocks are
exactly the same, so they should encrypt to the same thing. Thus, I can set a
counter to go through from i = 0 to 255 and break when the second block of the
encrypted text equals the second block of the old encrypted text. This can give
me the first byte of the message.
Now I can do this for the second one too. I need to record the second block of this result:
Secret starts here starts here
V
G E T / A A A A A A A A A A | A A A A A A A A A A A A A A A A A A A 1 2 | 3 4 5 ...
^ ^ ^
First block Second block Third block
And then since I know the first character of the secret from the previous round, I can put that in the message after the As
Secret starts here
V
G E T / A A A A A A A A A A | A A A A A A A A A A A A A A A A A A A 1 i | 1 2 3 ...
^ ^ ^
First block Second block Third block
Then I iterate through I until it encrypts to the same thing, and I have the second byte. Writing this up looks like this:
alphabet = '''_abcdefghijklmnopqrstuvwxyz\n\rABCDEFGHIJKLMNOPQRSTUVWXYZ/.\t !"#$%&\'()*+,-0123456789:;<=>?@[\\]^`{|}~\x7f\x80\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff'''
for i in range(0, 16):
# one iteration takes about 20 seconds
target = send('A' * (11 + 15 - i))[16:32]
for test_char in alphabet:
resp = send('A' * (11 + 15 - i) + known + test_char)[16:32]
if resp == target:
known += test_char
print (repr(known))
break
else:
print ('no answer for', i)
That should yield the first block of the secret: ' HTTP/1.1\r\nCooki'
. That
looks a lot like an HTTP request. Seeing this, I have written the alphabet that
includes all ascii characters in the order of prominence (as predicted). This
new alphabet increases the speed at which we are able to get characters,
because the common characters get guessed first.
If you want to start getting the second block, you simply look at the third block when it has 15 characters you know, and 1 you don't. You have to get the key sequentially. Lets say you already know 15 letters via the method described above.
Secret starts here
V
G E T / A A A A A A A A A A | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 17 18 ...
^ ^ ^
First block Second block Third block
Then you remember the second block of the return. Rember that you already know the first 15 letters from the previous method. You simply send those 15 letters back to the server plus another letter.
Secret starts here
V
G E T / A A A A A A A A A A | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 i | 1 2 3 ...
^ ^ ^
First block Second block Third block
Where the user input is 11 A
, the first 15 letters of the secret, and a guess
i
. The real secret get appended ongo that
for i in range(16, 32):
target = send('A' * (11 + 15 + 16 - i))[32:48]
for test_char in alphabet:
resp = send('A' * (11 + 15 + 16 - i) + known + test_char)[32:48]
if resp == target:
known += test_char
print (repr(known))
break
else:
print ('no answer for', i)
Here is the complete script that implements all of the concepts from this document. It gives this funny-looking output as it gets the flag, piece by piece:
' '
' H'
' HT'
' HTT'
' HTTP'
...
' HTTP/1.1\r\nCookie: flag=congrats_on_your_first_ecb_decryptio'
' HTTP/1.1\r\nCookie: flag=congrats_on_your_first_ecb_decryption'
' HTTP/1.1\r\nCookie: flag=congrats_on_your_first_ecb_decryption\r'
' HTTP/1.1\r\nCookie: flag=congrats_on_your_first_ecb_decryption\r\n'
' HTTP/1.1\r\nCookie: flag=congrats_on_your_first_ecb_decryption\r\n\x01'
no answer for 63
' HTTP/1.1\r\nCookie: flag=congrats_on_your_first_ecb_decryption\r\n\x01'
I am not sure why it does not find an answer for 63. Perhaps I got the length of the message wrong.
congrats_on_your_first_ecb_decryption