> For the complete documentation index, see [llms.txt](https://www.ctfrecipes.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://www.ctfrecipes.com/cryptography/symmetric-cryptography/aes/mode-of-operation/cbc/padding-oracle/challenge-example.md).

# Challenge example

## Source code

```python
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.

{% code overflow="wrap" %}

```bash
$ 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
```

{% endcode %}

* The application returns an error in case of padding error.

{% code overflow="wrap" %}

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

{% endcode %}

### POC

As explained [here](/cryptography/symmetric-cryptography/aes/mode-of-operation/cbc/padding-oracle.md), 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`

{% code overflow="wrap" %}

```python
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
```

{% endcode %}

```bash
$ python3 example.py
Original block byte : b'\t'
bruteforce result : b'\x04'
last char : b'\x0c'
```

{% hint style="info" %}
here the byte `0x0c` was found, it means that there is probably a padding of size 13 on the plain text
{% endhint %}

### Full exploitation

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

```python
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}")
```

```bash
$ 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 ](https://github.com/mwielgoszewski/python-paddingoracle)python implementation of padBuster :

{% code overflow="wrap" %}

```python
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}")
```

{% endcode %}

{% code overflow="wrap" %}

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

{% endcode %}

## Exercice

If you want to try this exploit by yourself, you can pull [this docker image](https://hub.docker.com/r/thectfrecipes/crypto/) :

```
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
```
