A padding oracle attack exploits how a cryptographic system handles padding errors when decrypting ciphertext, allowing an attacker to decrypt arbitrary messages.

Imagine you have a secret message encrypted with AES in CBC mode. To decrypt it, the recipient needs to remove some padding that was added to make the plaintext a multiple of the block size. If the padding is incorrect, the decryption process will signal an error. A padding oracle is a system that reveals whether the padding was correct or not, without revealing the decrypted plaintext itself. This seemingly small piece of information is enough to launch a powerful attack.

Let’s see it in action. Suppose we have an encrypted message C and we want to decrypt it. We can’t directly decrypt C without knowing the key. But we can send modified versions of C to the decryption oracle.

Consider a block B_i of ciphertext. When decrypted, it’s XORed with the previous ciphertext block B_{i-1} to produce the plaintext block P_i. The last few bytes of P_i are the padding. If the padding is invalid, the oracle tells us.

Here’s the core idea: an attacker can take the last ciphertext block C_n and the second-to-last ciphertext block C_{n-1} and modify C_{n-1} slightly. Let’s call the modified block C'_{n-1}. The oracle will decrypt C_n using C'_{n-1}.

The decryption of C_n with C'_{n-1} yields P'_n. The padding check will depend on the last byte of P'_n. P'_n = Decrypt(K, C_n) XOR C'_{n-1}

If the padding is valid, it means the last byte of P'_n has a specific value (e.g., 0x01 for a single byte of padding). If it’s invalid, it has another value. By systematically changing the last byte of C'_{n-1}, we can make the last byte of P'_n cycle through all possible values. When the padding check passes, we know the last byte of P'_n.

Let P_n be the original plaintext block. P_n = Decrypt(K, C_n) XOR C_{n-1}

We want to find P_n. We know Decrypt(K, C_n). Let’s call this D_n. P_n = D_n XOR C_{n-1}

Our modified decryption gives P'_n = D_n XOR C'_{n-1}. If we know P'_n, we can find D_n: D_n = P'_n XOR C'_{n-1}. And then we can find the original plaintext P_n: P_n = (P'_n XOR C'_{n-1}) XOR C_{n-1}.

The attack works by modifying C'_{n-1} such that the last byte of P'_n results in valid padding. Let the last byte of C_{n-1} be c_{n-1, last} and the last byte of C'_{n-1} be c'_{n-1, last}. Let the last byte of D_n be d_{n, last}. The last byte of P_n is p_{n, last} = d_{n, last} XOR c_{n-1, last}.

If we want the last byte of P'_n to be 0x01 (for valid padding of length 1), then: 0x01 = d_{n, last} XOR c'_{n-1, last}

We can iterate through all 256 possible values for c'_{n-1, last}. When the oracle reports valid padding, we’ve found the value of c'_{n-1, last} that makes the last byte of P'_n equal to 0x01. Let this specific value be c'_{n-1, last, correct}. Then 0x01 = d_{n, last} XOR c'_{n-1, last, correct}. We can solve for d_{n, last}: d_{n, last} = 0x01 XOR c'_{n-1, last, correct}.

Now that we know the last byte of D_n, we can use it to find the second to last byte of the original plaintext. Suppose we want to find p_{n, second_last}. p_{n, second_last} = d_{n, second_last} XOR c_{n-1, second_last}. We don’t know d_{n, second_last} yet.

However, we can modify C'_{n-1} differently. Instead of aiming for valid padding of length 1, we can aim for valid padding of length 2. This means the last two bytes of P'_n must be 0x0202. P'_n = D_n XOR C'_{n-1}

So, the last two bytes of P'_n must be 0x0202. p'_{n, last} = 0x02 p'_{n, second_last} = 0x02

This implies: 0x02 = d_{n, last} XOR c'_{n-1, last} 0x02 = d_{n, second_last} XOR c'_{n-1, second_last}

We already know d_{n, last} from the previous step. We can use this to find the correct c'_{n-1, last} that yields 0x02 for the last byte. c'_{n-1, last} = 0x02 XOR d_{n, last}

Now, we can iterate through all 256 possible values for c'_{n-1, second_last}. When the oracle reports valid padding (meaning the last two bytes of P'_n are 0x0202), we have found the correct c'_{n-1, second_last}. Let this be c'_{n-1, second_last, correct}. Then, 0x02 = d_{n, second_last} XOR c'_{n-1, second_last, correct}. We can solve for d_{n, second_last}: d_{n, second_last} = 0x02 XOR c'_{n-1, second_last, correct}.

By repeating this process, byte by byte, we can recover the entire plaintext block P_n. To decrypt the previous block P_{n-1}, we would use C_{n-1} and C_{n-2} in a similar fashion. The attacker essentially uses the oracle to find Decrypt(K, C_i) for each block i.

The key takeaway is that the attacker doesn’t need the decryption key. They only need a way to submit modified ciphertexts and observe whether the padding is valid or not. This is why systems that leak padding information are vulnerable.

The most surprising true thing about padding oracle attacks is that they can be used to decrypt arbitrary messages, not just those you intercept. If an attacker can trick a victim into submitting a crafted message through a vulnerable system, they can decrypt that message as if they had the key.

Here’s a simplified representation of the attack on a single block C_n using C_{n-1}:

  1. Target: Recover P_n = Decrypt(K, C_n) XOR C_{n-1}.
  2. Oracle’s Role: Submit (C_i, C_{i+1}) and get True if Decrypt(K, C_{i+1}) XOR C_i has valid padding, False otherwise.
  3. Attack Step (Recovering last byte of Decrypt(K, C_n)):
    • Choose target_padding = 1.
    • Iterate c'_{n-1, last} from 0x00 to 0xFF.
    • For each c'_{n-1, last}, construct C'_{n-1} where only the last byte is modified.
    • Submit (C'_{n-1}, C_n) to the oracle.
    • If the oracle returns True, it means Decrypt(K, C_n) XOR C'_{n-1} ends with 0x01.
    • Let d_{n, last} be the last byte of Decrypt(K, C_n).
    • We have 0x01 = d_{n, last} XOR c'_{n-1, last, correct}.
    • So, d_{n, last} = 0x01 XOR c'_{n-1, last, correct}. This recovers the last byte of the intermediate decryption result.
  4. Attack Step (Recovering second-to-last byte):
    • Choose target_padding = 2.
    • We know d_{n, last}.
    • We need Decrypt(K, C_n) XOR C'_{n-1} to end with 0x0202.
    • This means:
      • d_{n, last} XOR c'_{n-1, last} = 0x02
      • d_{n, second_last} XOR c'_{n-1, second_last} = 0x02
    • From the first equation, we can determine the required c'_{n-1, last}: c'_{n-1, last} = 0x02 XOR d_{n, last}.
    • Now, iterate c'_{n-1, second_last} from 0x00 to 0xFF.
    • Construct C'_{n-1} with the determined c'_{n-1, last} and the current c'_{n-1, second_last}.
    • Submit (C'_{n-1}, C_n) to the oracle.
    • If the oracle returns True, it means Decrypt(K, C_n) XOR C'_{n-1} ends with 0x0202.
    • We have 0x02 = d_{n, second_last} XOR c'_{n-1, second_last, correct}.
    • So, d_{n, second_last} = 0x02 XOR c'_{n-1, second_last, correct}. This recovers the second-to-last byte.
  5. Continue: Repeat for all bytes of the block, increasing the target padding length each time. Once P_n is recovered, move to the previous block.

The system in action isn’t about seeing encrypted data, but about observing the oracle’s responses. A vulnerable web application might, for example, return an error page for incorrect padding and a success page for correct padding, leaking the required information.

The fundamental problem this solves is the secure transmission of data where the recipient cannot trivially verify the integrity of the encrypted message beyond the decryption process itself. CBC mode, without proper integrity checks, is susceptible.

The exact levers you control are the padding scheme and, crucially, how decryption errors are reported. If you use AES-CBC, you must pair it with a Message Authentication Code (MAC) such as HMAC, and verify the MAC before attempting decryption. This is known as Encrypt-then-MAC. Decrypt-then-MAC and MAC-then-Encrypt are vulnerable to padding oracle attacks.

The one thing most people don’t know is that you don’t need to recover the entire plaintext block at once. You can recover one byte at a time. For the Nth byte (from the end, 1-indexed), you aim for a padding of N. This means the last N bytes of the decrypted plaintext block must be N repeated N times. You then manipulate the corresponding ciphertext block’s last N bytes to make this happen, using the previously recovered intermediate decryption bytes to fix the earlier bytes of the modified ciphertext.

The next concept you’ll run into is how to mitigate these attacks, primarily through the use of authenticated encryption modes.

Want structured learning?

Take the full Cryptography course →