Challenge example

Source code

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import json
import os

FLAG = ?
KEY = ?

def encryptFlag():
    data = {"flag": FLAG}

    plaintext = json.dumps(data).encode()
    padded = pad(plaintext, 16)
    iv = os.urandom(16)
    cipher = AES.new(KEY, AES.MODE_CBC, iv)
    try:
        encrypted = iv + cipher.encrypt(padded)
        print(f"Here is your token : {encrypted.hex()}")
    except ValueError as e:
        print({"error": str(e)})
        return

def checkToken():
    token = input("Token : ")
    token = bytes.fromhex(token)
    iv = token[:16]
    token = token[16:]

    cipher = AES.new(KEY, AES.MODE_CBC, iv)
    try:
        plaintext = cipher.decrypt(token)
        unpadded = unpad(plaintext, 16)
        data = json.loads(unpadded)
    except ValueError as e:
        print({"error": str(e)})
        return

    if data["flag"] == FLAG:
        print("Token is valid")


print("Welcome !")
encryptFlag()
checkToken()

The objective is to break the ciphertext to retrieve the flag value.

Exploitation

Vulnerability detection

In order to exploit the padding oracle attack, there are 2 things that need to be checked:

  • We can send as many time as we want a ciphertext to decrypt.

$ nc 127.0.0.1 1337
Welcome !
Here is your token : 218b43506e242a8be58b281d147a75f2709c2695687e6c42791677a4cc9e3a391873aeb156a1f1f3bbc0b9d1ab21d509687606b976351f4998d4210c0e875930
Token : 218b43506e242a8be58b281d147a75f2709c2695687e6c42791677a4cc9e3a391873aeb156a1f1f3bbc0b9d1ab21d509687606b976351f4998d4210c0e875930
Token is valid

$ nc 127.0.0.1 1337
Welcome !
Here is your token : c5bf8a9b3e59cd2d80e06a99bc36cbdefea51ee96ae74ac53ecc2e54da7a229e60b7cceacc4d8b96b490f68aa3a800975679289a314ea455c58516947899f216
Token : 218b43506e242a8be58b281d147a75f2709c2695687e6c42791677a4cc9e3a391873aeb156a1f1f3bbc0b9d1ab21d509687606b976351f4998d4210c0e875930
Token is valid
  • The application returns an error in case of padding error.

$ nc 127.0.0.1 1337
Welcome !
Here is your token : e7f357f5b63a730c095a9f7d3ff834e3786deed41d4b16983591ace7c4698ce205cfe4ab168ea4c6abdc58f3cae7bf425ab76dc95c03747f66dfd86edc955ae1
Token : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
{'error': 'Padding is incorrect.'}

POC

As explained here, in order to retrieve the last character of the last block, we need to brute force the last character of the penultimate block until the application returns a valid padding.

Then the real plaintext byte will be bruteforce_result ⊕ original_block_byte ⊕ 0x01

from pwn import *
from rich.console import Console

console = Console()
context.log_level = 'error'

def checkToken(token):
    p = remote("127.0.0.1", 1337)
    p.recvuntil(b"Token :")
    p.sendline(token.encode())

    resp = p.clean()

    p.close()
    return resp

token = "218b43506e242a8be58b281d147a75f2709c2695687e6c42791677a4cc9e3a391873aeb156a1f1f3bbc0b9d1ab21d509687606b976351f4998d4210c0e875930"

blocks = [token[i:i+32] for i in range(0,len(token),32)]

# Starting the console status to indicate the progress
with console.status(f"Trying byte : ") as a:

    # Creating a string with 30 zeros, which corresponds to a block of 15 null bytes
    block_attack = "00" * 15

    # Extracting the last byte of the second-to-last block
    original_block_byte = bytes.fromhex(blocks[-2][-2:])
    print(f"Original block byte : {original_block_byte}")

    # Looping through all possible values of a byte (0-255)
    for c in range(255):
        
        # Converting the byte value to bytes
        c = bytes([c])

        # Replacing the second-to-last block of the ciphertext by the arbitrary forged block
        blocks[-2] = block_attack + c.hex()

        # Sending the modified ciphertext to the server to check if padding is valid
        resp = checkToken("".join(blocks))

        # Updating the console status to show the current byte value being tried
        a.update(f"Trying byte : {c}")

        # If the server does not return a padding error, the guessed byte is correct
        if b'Padding' not in resp and b"padding" not in resp:

            # Printing the result of the brute force
            print(f"bruteforce result : {c}")

            # XORing the guessed byte with the original byte to get the plaintext
            last_char = xor(c, original_block_byte, b"\x01")
            print(f"last char : {last_char}")

            # Exiting the loop as the last character has been found
            break
$ python3 example.py
Original block byte : b'\t'
bruteforce result : b'\x04'
last char : b'\x0c'

here the byte 0x0c was found, it means that there is probably a padding of size 13 on the plain text

Full exploitation

We just have to update the code to make it follow the entire padding oracle algorithm

from pwn import *
from rich.console import Console
import string
import json
import copy

console = Console()
context.log_level = 'error'

def getToken():

    p = remote("127.0.0.1", 1337)
    p.recvuntil(b": ")
    token = p.recvline()[:-1].decode()

    p.close()
    return token


def checkToken(token):
    p = remote("127.0.0.1", 1337)
    p.recvuntil(b"Token :")
    p.sendline(token.encode())

    resp = p.clean()

    p.close()
    return resp

token = bytes.fromhex(getToken())

blocks = [token[i:i+16] for i in range(0,len(token),16)]

plain = b""
with console.status(f"Trying byte : ") as a:
    for i in range(len(blocks)-1):
        arbitrary = copy.copy(blocks)
        for b in range(16):
            cur_plain = b"\x00" * (16-b) + plain[-16*(i+1):len(plain)-16*i]
            trail = xor(b+1, cur_plain, blocks[-2])[16-b:]
            
            for c in range(0,255):
                c = bytes([c])
                block_attack = (15 - b) * b'\x00' + c + trail
                arbitrary[-2] = block_attack
                test = b''.join(arbitrary).hex()

                a.update(f"clear = {plain}\nblock_attack = {block_attack}\ntoken = {test}\nTrying byte : {c}")
                r = checkToken(test)

                if b'Padding' not in r and b"padding" not in r:
                    plain_byte = xor(c, b+1, blocks[-2][-b-1])
                    plain = plain_byte + plain
                    break

        blocks = blocks[:-1]

print(f"clear = {plain}")
$ python3 exploit.py
 clear = b'}"}\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c'
block_attack = b"\x08x\x93\xc6CS\x89\xfd'mY\xad\xe66!\xc2"
token = 08b9c82b87335577d4fc953594fd96ae612c3c8c0b29fc602bf2f08fac717aaf087893c6435389fd276d59ade63621c246a0379337609b70da2d2d0558326511
Trying byte : b'\x08'

...

 clear = b'esome}"}\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c'
block_attack = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00U\xcc\x07\x10\xc7'
token = 08b9c82b87335577d4fc953594fd96ae000000000000000000000055cc0710c7c915a1ab5f4f95e13b7145b1fa2a3dde
Trying byte : b'U'

...

clear = b'{"flag": "FLAG{CBCOracleIsAwesome}"}\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c'

Using Libraries

Many people have write libraries to automate this attack. Here is an example using this python implementation of padBuster :

from pwn import *
from paddingoracle.paddingoracle import BadPaddingException, PaddingOracle
from Crypto.Cipher import AES

context.log_level = 'error'
class PadBuster(PaddingOracle):
    def oracle(self, data):
        while True:
            try:
                r = remote("127.0.0.1", 1337)
                r.recvuntil(b"Token : ")
                s = data.hex().encode()
                r.sendline(s)
                out = r.recvall()
                if b"padding" in out or b"Padding" in out:
                    raise BadPaddingException
                return
            except (socket.error, socket.gaierror, socket.herror, socket.timeout) as e:
                print(e)

if __name__ == '__main__':
    token = bytes.fromhex("c5bf8a9b3e59cd2d80e06a99bc36cbdefea51ee96ae74ac53ecc2e54da7a229e60b7cceacc4d8b96b490f68aa3a800975679289a314ea455c58516947899f216")
    padbuster = PadBuster()
    decrypted = padbuster.decrypt(token[16:], block_size=AES.block_size, iv=token[:16])

    print(f"Clear : {decrypted}")
$ python3 exploit_lib.py
Clear : bytearray(b'{"flag": "FLAG{CBCOracleIsAwesome}"}\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c')

Exercice

If you want to try this exploit by yourself, you can pull this docker image :

docker pull thectfrecipes/crypto:CBCOracle

Deploy the image using the followed command :

docker run --name thectfrecipes_crypto_CBCOracle -it --rm -d -p 1337:1337 thectfrecipes/crypto:CBCOracle

The service is available on port 1337

nc 127.0.0.1 1337

Last updated