Enchanted Oracle - UofT CTF 2025
Challenge:
from base64 import b64encode, b64decode
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
print("Welcome to the AES-CBC oracle!")
key = open("key", "rb").read()
while True:
print("Do you want to encrypt the flag or decrypt a message?")
print("1. Encrypt the flag")
print("2. Decrypt a message")
choice = input("Your choice: ")
if choice == "1":
cipher = AES.new(key=key, mode=AES.MODE_CBC)
ciphertext = cipher.iv + \
cipher.encrypt(pad(b"random", cipher.block_size))
print(f"{b64encode(ciphertext).decode()}")
elif choice == "2":
line = input().strip()
data = b64decode(line)
iv, ciphertext = data[:16], data[16:]
cipher = AES.new(key=key, mode=AES.MODE_CBC, iv=iv)
try:
plaintext = unpad(cipher.decrypt(ciphertext),
cipher.block_size).decode('latin1')
except Exception as e:
print("Error!")
continue
if plaintext == "I am an authenticated admin, please give me the flag":
print("Victory! Your flag:")
print(open("flag.txt").read())
else:
print("Unknown command!")
Solve:
So basically we need to do a ciphertext forgery. In fact, choice 1 is not needed at all!
I split the plaintext into its 4 16-byte blocks (p1, p2, p3, p4) with corresponding ciphertext blocks (c1, c2, c3, c4).
We want to choose c4 at random and then work backwards.
For example:
from Crypto.Util.Padding import pad, unpad
from Crypto.Cipher import AES
from os import urandom
from pwn import xor
KEY = urandom(16)
plaintext = b"I am an authenticated admin, please give me the flag"
p1 = b'I am an authenti'
p2 = b'cated admin, ple'
p3 = b'ase give me the '
p4 = pad(b'flag', 16)
assert plaintext == unpad(p1+p2+p3+p4, 16)
def ECB_dec(x):
return AES.new(KEY, AES.MODE_ECB).decrypt(x)
c4 = urandom(16)
c3 = xor(p4, ECB_dec(c4))
c2 = xor(p3, ECB_dec(c3))
c1 = xor(p2, ECB_dec(c2))
forged_iv = xor(p1, ECB_dec(c1))
forged_ct = c1+c2+c3+c4
print(unpad(AES.new(key=KEY, mode=AES.MODE_CBC, iv=forged_iv).decrypt(forged_ct), 16))
Now, just swap out the ECB_dec function for the classic CBC padding attack!
And implement batched requests for a big speedup.
Final solver:
from Crypto.Util.Padding import pad
from base64 import b64encode
from os import urandom
from pwn import xor, process, remote
from tqdm import trange
def attack(c):
r = b''
for i in trange(15, -1, -1):
s = bytes([16 - i] * (16 - i))
payload = b''
for b in range(256):
iv_ = b'\x00'*i + xor(s, bytes([b]) + r)
payload += b'2\n'
payload += b64encode(iv_ + c) + b'\n'
io.send(payload)
out = io.recvlines(numlines=256*4)
b = out[::4].index(b'Your choice: Unknown command!')
r = bytes([b]) + r
return r
p1 = b'I am an authenti'
p2 = b'cated admin, ple'
p3 = b'ase give me the '
p4 = pad(b'flag', 16)
#io = process(['python', 'aes-cbc.py'])
io = remote('34.162.82.42', 5000)
io.recv()
c4 = urandom(16)
c3 = xor(p4, attack(c4))
c2 = xor(p3, attack(c3))
c1 = xor(p2, attack(c2))
forged_iv = xor(p1, attack(c1))
forged_ct = c1+c2+c3+c4
io.sendline(b'2')
io.sendline(b64encode(forged_iv + forged_ct))
io.interactive()
# uoftctf{y3s_1_kn3w_y0u_w3r3_w0r7hy!!!}