I have this online service, that executes my commands. But only with the correct key can you craft a proper command. Everything else gets rejected.
nc 52.59.124.14 5102
The attachment provides the server implementation:
from Crypto.Cipher import AES
import os
import json
class PaddingError(Exception):
pass
flag = open('flag.txt','r').read().strip()
key = os.urandom(16)
def unpad(msg : bytes):
pad_byte = msg[-1]
if pad_byte == 0 or pad_byte > 16: raise PaddingError
for i in range(1, pad_byte+1):
if msg[-i] != pad_byte: raise PaddingError
return msg[:-pad_byte]
def decrypt(cipher : bytes):
if len(cipher) % 16 > 0: raise PaddingError
decrypter = AES.new(key, AES.MODE_CBC, iv = cipher[:16])
msg_raw = decrypter.decrypt(cipher[16:])
return unpad(msg_raw)
if __name__ == '__main__':
while True:
try:
cipher_hex = input('input cipher (hex): ')
if cipher_hex == 'exit': break
cipher = decrypt(bytes.fromhex(cipher_hex))
json_token = json.loads(cipher.decode())
eval(json_token['command'])
except PaddingError:
print('invalid padding')
except (json.JSONDecodeError, UnicodeDecodeError):
print('no valid json')
except:
print('something else went wrong')
It is prone to padding oracle attack: we can know if the padding is invalid or not. Through padding oracle attack, we can find the plaintext given ciphertext. I made a video on padding oracle attack in Chinese previously.
Since we want to read the flag out, we want the decrypted string to be {"command":"print(flag)"}
with padding added. It needs two AES blocks to save it. So:
{"command":"prin
, expected1 = t(flag)"}\x07\x07\x07\x07\x07\x07\x07
known1 = AES-ECB-Decrypt(key, msg1)
, where msg1 can be anything, e.g. all zerosmsg2 = known1 xor expected1
known2 = AES-ECB-Decrypt(key, msg2)
iv = known2 xor expected2
Concat them together:
AES-CBC-Decrypt(key, iv, msg2 || msg1)
= (iv xor AES-ECB-Decrypt(key, msg2)) || (msg2 xor AES-ECB-Decrypt(key, msg1))
= (iv xor known2) || (msg2 xor known1)
= (expected2) || (expected1)
= expected
Attack code:
from pwn import *
# context(log_level="debug")
# p = process(["python3", "chall.py"])
p = remote(host="52.59.124.14", port=5102)
def find(msg):
iv = [0] * 16
known = [0] * 16
# padding oracle attack
for i in range(1, 17):
good = []
for j in range(1, i):
iv[16 - j] = known[16 - j] ^ i
for ch in range(256):
iv[16 - i] = ch
p.recvuntil("cipher")
p.sendline(bytes(iv + msg).hex())
res = p.recvline()
if b"no valid json" in res:
good.append(ch)
if len(good) == 1:
known[16 - i] = i ^ good[0]
else:
print(good)
assert False
return known
# msg1 encrypts to known1
msg1 = [0] * 16
known1 = find(msg1)
expected = bytearray(b'{"command":"print(flag)"}')
# add padding
padding = 16 - len(expected) % 16
for i in range(padding):
expected.append(padding)
# msg2 encrypts to known2
msg2 = [expected[16 + i] ^ known1[i] for i in range(16)]
known2 = find(msg2)
iv = [expected[i] ^ known2[i] for i in range(16)]
p.recvuntil("cipher")
p.sendline(bytes(iv + msg2 + msg1).hex())
p.interactive()
I run it in an AWS instance in eu-central-1 region to make it run faster.
Get flag: ENO{the_oracle_can_also_create_messages_as_desired}
.