Took a look into the code, the key was used for both key and IV for AES-CBC encryption and decryption, get_flag needed the key to get the flag. Function receive decrypts given cipher text, if failed, the decrypted message returned.
@chal.route('/lazy_cbc/encrypt/<plaintext>/')
def encrypt(plaintext):
plaintext = bytes.fromhex(plaintext)
if len(plaintext) % 16 != 0:
return {"error": "Data length must be multiple of 16"}
cipher = AES.new(KEY, AES.MODE_CBC, KEY)
encrypted = cipher.encrypt(plaintext)
return {"ciphertext": encrypted.hex()}
@chal.route('/lazy_cbc/get_flag/<key>/')
def get_flag(key):
key = bytes.fromhex(key)
if key == KEY:
return {"plaintext": FLAG.encode().hex()}
else:
return {"error": "invalid key"}
@chal.route('/lazy_cbc/receive/<ciphertext>/')
def receive(ciphertext):
ciphertext = bytes.fromhex(ciphertext)
if len(ciphertext) % 16 != 0:
return {"error": "Data length must be multiple of 16"}
cipher = AES.new(KEY, AES.MODE_CBC, KEY)
decrypted = cipher.decrypt(ciphertext)
try:
decrypted.decode() # ensure plaintext is valid ascii
except UnicodeDecodeError:
return {"error": "Invalid plaintext: " + decrypted.hex()}
return {"success": "Your message has been received"}
Time to do some math. According to AES-CBC decription process, we have the following equations. pn for the nth block of plain text, cn for the nth block of cipher text, d() is the decryption function.
key = iv = d(c0) ^ p0
p0 = d(c0) ^ iv
p1 = d(c1) ^ c0
p2 = d(c2) ^ c1
If c1 = 0 and c2 = c0, the equations above become
p0 = d(c0) ^ iv
p1 = d(0) ^ c0
p2 = d(c0) ^ 0
If xor p0 and p2, since key is used as IV, we have the following transformation to get the key.
p0 ^ p2 = d(c0) ^ iv ^ d(c0)
= iv = key
Made up plain text that takes 3 blocks.
plain = (b'a'*(16*3)).hex()
Encrypted it we have cipher text
cipher = '1c5ded2c669062d2cd3a11766371be1a38f0a5d3c96961eac8586bb4549dfc41c49a8a3d4c17740bf224d19d129fa9a8'
Altered the cipher text so that the second block is filled with zeroes and the third block equal to the first one.
fake_cipher = cipher[:32] + '0'*32 + cipher[:32]
Attempted to decrypt the fake cipher text just made up, the serve gave this error with decrypted message.
{"error":"Invalid plaintext: 6161616161616161616161616161616155cb30af3a7c7a40f8ce7e766c8037579bf317d1684a16e1e95691b163dc178a"}
Xor'd the first block and the third block of the fake plain text to get IV, as well as the key, which is fa9276b0092b77808837f0d002bd76eb.
fake_plain = '6161616161616161616161616161616155cb30af3a7c7a40f8ce7e766c8037579bf317d1684a16e1e95691b163dc178a'
fake_plain = bytes.fromhex(fake_plain)
iv = [0]*16
for i in range(len(iv)):
iv[i] = fake_plain[i] ^ fake_plain[32+i]
Requested get_flag
with the key, the flag returned in hex. Decoded it to be crypto{50m3_p30pl3_d0n7_7h1nk_IV_15_1mp0r74n7_?}
.
flag = '63727970746f7b35306d335f703330706c335f64306e375f3768316e6b5f49565f31355f316d70307237346e375f3f7d'
print(bytes.fromhex(flag))