In this challenge we have a server that allows us to encrypt text in CBC mode, but we know that :

from Crypto.Cipher import AES
 
 
KEY = ?
FLAG = ?
 
 
@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"}
 

In order to understand how to exploit this vulnerability, we need to look at how CBC decryption is made. Suppose that we have a plaintext of three blocks:

An attacker could tamper the ciphertext to have:

And this way, the decryption becomes:

This way XORing gives:

Solution described here

An alternative solution would require an attacker to send a ciphertext of two blocks, both at . This way we have

now XORing gives: