Challenge Overview
Category: Cryptography Objective: Exploit a flaw in a custom Triple DES encryption oracle to recover the secret key and decrypt the flag.
The server allows us to:
- Choose
k2andk3(both 8-byte DES keys with odd parity) - Select one of three encryption modes
- Either encrypt the flag or encrypt our own plaintext
Key Observations
k1is fixed throughout the session but unknown to us.ultra_secure_v1: - Triple encryption (EEE mode).ultra_secure_v2: - Decrypt with secret key as outer operation!
1. Vulnerability
The critical vulnerability lies in triple_des_ultra_secure_v2:
Since we control k2, k3, and the input pt, we can make equal any value we want. This effectively gives us a decryption oracle for the secret key !
2. Exploitation Strategy
Attack Flow
-
Get encrypted flag using
ultra_secure_v1with our chosen keys , : where -
Find auxiliary keys , such that ends with PKCS7 padding
\x01:- This allows us to strip the last byte and send 15 bytes to the oracle.
- The server will re-add the padding, resulting in exactly the value we want.
-
Use
ultra_secure_v2as decryption oracle:- Send (15 bytes).
- Server pads it back to 16 bytes with
\x01. - Server returns .
-
Recover the flag using the known keys , : Then unpad to get the flag.
Why the Padding Trick?
The server always pads input with pad(pt, 8). To send exactly the value we want to the decryption oracle, we need to work around this padding. By finding keys where ends with \x01:
- We strip that byte (15 bytes remain).
- The server adds
\x01back (16 bytes, original value restored). - The decryption oracle gives us exactly .
3. Exploit Code
#!/usr/bin/env python3from Crypto.Cipher import DESfrom Crypto.Util.Padding import unpadfrom pwn import remote
def adjust_key(key8: bytes) -> bytes: out = bytearray() for b in key8: b7 = b & 0xFE ones = bin(b7).count("1") out.append(b7 | (ones % 2 == 0)) return bytes(out)
def des_encrypt(key, data): cipher = DES.new(key, DES.MODE_ECB) return cipher.encrypt(data)
def des_decrypt(key, data): cipher = DES.new(key, DES.MODE_ECB) return cipher.decrypt(data)
def exploit(): io = remote("20.193.149.152", 1340)
# Step 1: Choose arbitrary k2, k3 and get encrypted flag k2_raw = b'\x01' * 8 k3_raw = b'\x02' * 8 k2 = adjust_key(k2_raw) k3 = adjust_key(k3_raw)
io.recvuntil(b"enter k2 hex bytes >") io.sendline(k2.hex().encode()) io.recvuntil(b"enter k3 hex bytes >") io.sendline(k3.hex().encode()) io.recvuntil(b"4. exit") io.sendline(b"2") # ultra secure v1 io.recvuntil(b"enter option >") io.sendline(b"1") # encrypt flag
io.recvuntil(b"ciphertext : ") c1 = bytes.fromhex(io.recvline().strip().decode()) print(f"[*] C1 = {c1.hex()}") # C1 = E_k1(E_k2(E_k3(F)))
# Step 2: Find k2', k3' where D_k3'(D_k2'(C1)) ends with 0x01 print("[*] Brute-forcing auxiliary keys...") for i in range(256): for j in range(256): k2p = adjust_key(bytes([i] * 8)) k3p = adjust_key(bytes([j] * 8)) x = des_decrypt(k3p, des_decrypt(k2p, c1)) if x[-1:] == b'\x01': p_full, k2p, k3p = x, k2p, k3p print(f"[*] Found keys at ({i}, {j})") break else: continue break
# Step 3: Use ultra_secure_v2 as decryption oracle p = p_full[:-1] # Strip the padding byte
io.recvuntil(b"enter k2 hex bytes >") io.sendline(k2p.hex().encode()) io.recvuntil(b"enter k3 hex bytes >") io.sendline(k3p.hex().encode()) io.recvuntil(b"4. exit") io.sendline(b"3") # ultra secure v2 io.recvuntil(b"enter option >") io.sendline(b"2") # encrypt own text io.recvuntil(b"enter hex bytes >") io.sendline(p.hex().encode())
io.recvuntil(b"ciphertext : ") c2 = bytes.fromhex(io.recvline().strip().decode())[:len(c1)] print(f"[*] C2 = {c2.hex()}") # C2 = D_k1(C1) = E_k2(E_k3(F))
# Step 4: Decrypt using known k2, k3 flag = unpad(des_decrypt(k3, des_decrypt(k2, c2)), 8) print(f"[+] Flag: {flag.decode()}") io.close()
if __name__ == "__main__": exploit()4. Flag
BITSCTF{5up3r_d35_1z_n07_53cur3}