Writeup - UMDCTF 2024: PaddingOracle
A crypto challenge by lily, solved by Trayshar and Euph0r14
Challenge
The Baron used AES128-CBC with PKCS#7 to hide the flag. Can you recover the flag using his padding oracle?
We are given a server to connect to via netcat, the encrypted flag and the Initialization Vector used to encrypt it. The Encryption used is AES128 with PKCS#7 padding in CBC mode. We can send a ciphertext to the server, and it returns whether it has valid padding.
Padding Oracle
As the name of the challenge suggests, a Padding Oracle Attack seemed like the way to go. The link above explains it better than I can, so go read it if you aren’t familiar with this attack.
In short, given the IV and the ciphertext of the flag, we can send a carefully crafted ciphertext to the server (“query the oracle”) to eventually learn one byte of plaintext.
Implementation
I basically copied their sample implementation. The only issue was the connection to the server; After some time, it closed, so I had to open a new connection to continue. As the attack works byte-by-byte, I could restart at the position of the last solved byte, which didn’t lose much progress.
1#!/usr/bin/env python3
2
3from pwn import *
4
5# Copied from https://research.nccgroup.com/2021/02/17/cryptopals-exploiting-cbc-padding-oracles/
6BLOCK_SIZE = 16
7def single_block_attack(block, oracle):
8 """Returns the decryption of the given ciphertext block"""
9
10 # zeroing_iv starts out nulled. each iteration of the main loop will add
11 # one byte to it, working from right to left, until it is fully populated,
12 # at which point it contains the result of DEC(ct_block)
13 zeroing_iv = [0] * BLOCK_SIZE
14
15 for pad_val in range(1, BLOCK_SIZE+1):
16 padding_iv = [pad_val ^ b for b in zeroing_iv]
17
18 for candidate in range(256):
19 padding_iv[-pad_val] = candidate
20 iv = bytes(padding_iv)
21 if oracle(iv, block):
22 if pad_val == 1:
23 # make sure the padding really is of length 1 by changing
24 # the penultimate block and querying the oracle again
25 padding_iv[-2] ^= 1
26 iv = bytes(padding_iv)
27 if not oracle(iv, block):
28 continue # false positive; keep searching
29 break
30 else:
31 raise Exception("no valid padding byte found (is the oracle working correctly?)")
32 zeroing_iv[-pad_val] = candidate ^ pad_val
33
34 return zeroing_iv
35
36# Copied from https://research.nccgroup.com/2021/02/17/cryptopals-exploiting-cbc-padding-oracles/
37def full_attack(iv, ct, oracle):
38 """Given the iv, ciphertext, and a padding oracle, finds and returns the plaintext"""
39 assert len(iv) == BLOCK_SIZE and len(ct) % BLOCK_SIZE == 0
40
41 msg = iv + ct
42 blocks = [msg[i:i+BLOCK_SIZE] for i in range(0, len(msg), BLOCK_SIZE)]
43 result = b''
44
45 # loop over pairs of consecutive blocks performing CBC decryption on them
46 iv = blocks[0]
47 for ct in blocks[1:]:
48 dec = single_block_attack(ct, oracle)
49 pt = bytes(iv_byte ^ dec_byte for iv_byte, dec_byte in zip(iv, dec))
50 result += pt
51 iv = ct
52
53 return result
54
55
56
57conn = None
58def main():
59 # Set up the connection parameters
60 host = 'challs.umdctf.io' # Change this to the actual host
61 port = 32345 # Change this to the actual port
62
63 def reopen_connection():
64 global conn
65 if conn is not None:
66 conn.close()
67 # Connect to the host
68 conn = remote(host, port)
69 # Receive the response from the server
70 response = conn.recv()
71 print("Received:", response)
72
73 reopen_connection()
74
75 flag = "d697937950b3090d56828170609a3b23f836e3cc0ed631cb9ce08c4b9785f5f3db5dee5f44adaad3630303062b61d5fa"
76 flag = bytes.fromhex(flag)
77 iv = "2652b7ae08b281594c488cf2e6daee43"
78 iv = bytes.fromhex(iv)
79
80 bad_response = b"wrong ciphertext size!\n\n\n\ngive me a ciphertext and I'll tell you if the corresponding plaintext has valid padding:\n"
81 wrong_padding = b"invalid padding :(\n\n\n\ngive me a ciphertext and I'll tell you if the corresponding plaintext has valid padding:\n"
82
83 def oracle(iv: bytes, block: bytes):
84 global conn
85 try:
86 send_bytes = (iv + block).hex()
87 conn.send(send_bytes)
88 conn.send("\n")
89 print("Sent:", send_bytes)
90 # Receive the response from the server
91 response = conn.recv()
92 print("Received:", response[:30])
93 return response != wrong_padding
94 except EOFError:
95 # After some time the connection dies, so restart it while keeping state
96 print("Failed to send message, restarting connection!")
97 reopen_connection()
98 return oracle(iv, block)
99
100 solution = full_attack(iv, flag, oracle)
101 print(solution)
102
103 # Close the connection
104 conn.close()
105
106if __name__ == '__main__':
107 main()