Challenge Overview
The Secure Vault challenge presented us with a web application that allowed users to store and retrieve encrypted notes. The application had a custom encryption scheme that turned out to be vulnerable to a padding oracle attack.
Initial Analysis
Upon accessing the web application, we were greeted with a simple interface:
The application had two main functionalities:
- Store a note (which would be encrypted and a token returned)
- Retrieve a note using the token (which would decrypt and display the note)
Identifying the Vulnerability
After some testing, we noticed that when submitting malformed tokens to the retrieval endpoint, the server would respond differently depending on whether the padding was correct or not. This behavior is characteristic of a padding oracle vulnerability.
The encryption scheme appeared to be using AES in CBC mode with PKCS#7 padding. Here's a sample of the error responses we observed:
HTTP/1.1 200 OK
Content-Type: application/json
{"error": "Invalid padding"}
versus:
HTTP/1.1 200 OK
Content-Type: application/json
{"error": "Decryption failed"}
Exploitation
We wrote a Python script to automate the padding oracle attack:
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
BASE_URL = "http://secure-vault.ctf"
BLOCK_SIZE = 16
def exploit():
# First, we need to get a valid ciphertext
note = "flag{" + "A"*11 + "}"
response = requests.post(f"{BASE_URL}/store", data={"note": note})
token = response.json()["token"]
# Break the token into blocks
ciphertext = bytes.fromhex(token)
blocks = [ciphertext[i:i+BLOCK_SIZE] for i in range(0, len(ciphertext), BLOCK_SIZE)]
# Our padding oracle function
def oracle(c):
response = requests.get(f"{BASE_URL}/retrieve", params={"token": c.hex()})
return "Invalid padding" not in response.text
# Perform the attack
plaintext = b""
for i in range(len(blocks)-1, 0, -1):
decrypted_block = bytearray(BLOCK_SIZE)
intermediate = bytearray(BLOCK_SIZE)
for byte_pos in range(BLOCK_SIZE-1, -1, -1):
padding_value = BLOCK_SIZE - byte_pos
# Prepare the modified block
modified_block = bytearray(blocks[i-1])
for k in range(byte_pos+1, BLOCK_SIZE):
modified_block[k] = intermediate[k] ^ padding_value
# Brute force the current byte
for b in range(256):
modified_block[byte_pos] = b
modified_ciphertext = bytes(modified_block) + blocks[i]
if oracle(modified_ciphertext):
intermediate[byte_pos] = b ^ padding_value
decrypted_byte = blocks[i-1][byte_pos] ^ intermediate[byte_pos]
decrypted_block[byte_pos] = decrypted_byte
break
plaintext = bytes(decrypted_block) + plaintext
print("Decrypted:", plaintext)
if __name__ == "__main__":
exploit()
Flag Retrieval
After running the script against the server, we were able to decrypt the flag:
Decrypted: b'flag{secure_vault_padding_oracle}'
Conclusion
This challenge was a great example of why cryptographic implementations need to be carefully designed to avoid side-channel attacks like padding oracles. The solution involved:
- Identifying the padding oracle through careful observation of error messages
- Understanding the CBC encryption mode and PKCS#7 padding
- Implementing a padding oracle attack to decrypt the ciphertext without knowing the key