Pwn-la-Chapelle

Pwn-la-Chapelle

Popping Shells and Stealing Flags at RWTH Aachen University

Writeup - UMDCTF 2024: PaddingOracle

by Trayshar - - Estimated reading time: 4 minutes

A crypto challenge by lily, solved by Trayshar and Euph0r14

Challenge

The Baron used AES128-CBC with PKCS#7 to hide the flag. Can you recover the flag using his padding oracle?

We are given a server to connect to via netcat, the encrypted flag and the Initialization Vector used to encrypt it. The Encryption used is AES128 with PKCS#7 padding in CBC mode. We can send a ciphertext to the server, and it returns whether it has valid padding.

Padding Oracle

As the name of the challenge suggests, a Padding Oracle Attack seemed like the way to go. The link above explains it better than I can, so go read it if you aren’t familiar with this attack.

In short, given the IV and the ciphertext of the flag, we can send a carefully crafted ciphertext to the server (“query the oracle”) to eventually learn one byte of plaintext.

Implementation

I basically copied their sample implementation. The only issue was the connection to the server; After some time, it closed, so I had to open a new connection to continue. As the attack works byte-by-byte, I could restart at the position of the last solved byte, which didn’t lose much progress.

  1#!/usr/bin/env python3
  2
  3from pwn import *
  4
  5# Copied from https://research.nccgroup.com/2021/02/17/cryptopals-exploiting-cbc-padding-oracles/
  6BLOCK_SIZE = 16
  7def single_block_attack(block, oracle):
  8    """Returns the decryption of the given ciphertext block"""
  9
 10    # zeroing_iv starts out nulled. each iteration of the main loop will add
 11    # one byte to it, working from right to left, until it is fully populated,
 12    # at which point it contains the result of DEC(ct_block)
 13    zeroing_iv = [0] * BLOCK_SIZE
 14
 15    for pad_val in range(1, BLOCK_SIZE+1):
 16        padding_iv = [pad_val ^ b for b in zeroing_iv]
 17
 18        for candidate in range(256):
 19            padding_iv[-pad_val] = candidate
 20            iv = bytes(padding_iv)
 21            if oracle(iv, block):
 22                if pad_val == 1:
 23                    # make sure the padding really is of length 1 by changing
 24                    # the penultimate block and querying the oracle again
 25                    padding_iv[-2] ^= 1
 26                    iv = bytes(padding_iv)
 27                    if not oracle(iv, block):
 28                        continue  # false positive; keep searching
 29                break
 30        else:
 31            raise Exception("no valid padding byte found (is the oracle working correctly?)")
 32        zeroing_iv[-pad_val] = candidate ^ pad_val
 33
 34    return zeroing_iv
 35
 36# Copied from https://research.nccgroup.com/2021/02/17/cryptopals-exploiting-cbc-padding-oracles/
 37def full_attack(iv, ct, oracle):
 38    """Given the iv, ciphertext, and a padding oracle, finds and returns the plaintext"""
 39    assert len(iv) == BLOCK_SIZE and len(ct) % BLOCK_SIZE == 0
 40
 41    msg = iv + ct
 42    blocks = [msg[i:i+BLOCK_SIZE] for i in range(0, len(msg), BLOCK_SIZE)]
 43    result = b''
 44
 45    # loop over pairs of consecutive blocks performing CBC decryption on them
 46    iv = blocks[0]
 47    for ct in blocks[1:]:
 48        dec = single_block_attack(ct, oracle)
 49        pt = bytes(iv_byte ^ dec_byte for iv_byte, dec_byte in zip(iv, dec))
 50        result += pt
 51        iv = ct
 52
 53    return result
 54
 55
 56
 57conn = None
 58def main():
 59    # Set up the connection parameters
 60    host = 'challs.umdctf.io'  # Change this to the actual host
 61    port = 32345               # Change this to the actual port
 62
 63    def reopen_connection():
 64        global conn
 65        if conn is not None:
 66            conn.close()
 67        # Connect to the host
 68        conn = remote(host, port)
 69        # Receive the response from the server
 70        response = conn.recv()
 71        print("Received:", response)
 72
 73    reopen_connection()
 74
 75    flag = "d697937950b3090d56828170609a3b23f836e3cc0ed631cb9ce08c4b9785f5f3db5dee5f44adaad3630303062b61d5fa"
 76    flag = bytes.fromhex(flag)
 77    iv = "2652b7ae08b281594c488cf2e6daee43"
 78    iv = bytes.fromhex(iv)
 79
 80    bad_response = b"wrong ciphertext size!\n\n\n\ngive me a ciphertext and I'll tell you if the corresponding plaintext has valid padding:\n"
 81    wrong_padding = b"invalid padding :(\n\n\n\ngive me a ciphertext and I'll tell you if the corresponding plaintext has valid padding:\n"
 82
 83    def oracle(iv: bytes, block: bytes):
 84        global conn
 85        try:
 86            send_bytes = (iv + block).hex()
 87            conn.send(send_bytes)
 88            conn.send("\n")
 89            print("Sent:", send_bytes)
 90            # Receive the response from the server
 91            response = conn.recv()
 92            print("Received:", response[:30])
 93            return response != wrong_padding
 94        except EOFError:
 95            # After some time the connection dies, so restart it while keeping state
 96            print("Failed to send message, restarting connection!")
 97            reopen_connection()
 98            return oracle(iv, block)
 99
100    solution = full_attack(iv, flag, oracle)
101    print(solution)
102
103    # Close the connection
104    conn.close()
105
106if __name__ == '__main__':
107    main()
rssfacebooktwittergithubyoutubemailspotifylastfminstagramlinkedingooglegoogle-pluspinterestmediumvimeostackoverflowredditquoraquora