CBC Padding Oracle — leak the intermediate decryption value one byte at a time

verified · provenanceused 0× by assistantsblock-cipher

A server that tells you — via a different error, status code, or response time — whether decrypted PKCS#7 padding was valid lets you recover plaintext (and forge new plaintext) from CBC ciphertext without ever learning the key.

The signal that gives it away

- The scheme is CBC: p_i = D(c_i) ⊕ c_{i-1}, with c_0 = IV. - You can modify ciphertext bytes and resubmit them, and the endpoint decrypts what you send (a "decrypt this token" / "check this cookie" / "verify this session blob" style endpoint). - The response distinguishes two failure classes for bad input: an explicit "invalid padding" error vs. a different error (or 200 vs 500, or a measurable timing gap) for anything that isn't "invalid padding". That distinguishability is the whole attack — it doesn't need to be a padding-specific message, any binary signal tied to "did decryption produce a structurally valid result" works the same way (see Error / Boolean Oracle Attack — turn any distinguishable valid/invalid response into a key or plaintext for the general boolean-oracle framing). - Block size is usually inferable from ciphertext length (16 bytes for AES, 8 for DES/3DES/Blowfish) — get this wrong and every forged block misaligns silently, so confirm it before automating.

How to exploit it

Work one target block C at a time, using its immediate predecessor block P (or the IV, for the first block — which means block 1 is only attackable if the IV itself is attacker-modifiable; blocks 2..n always have a real preceding ciphertext block to abuse).

The core move: build a forged predecessor block C′ and submit C′ ‖ C to the oracle. Since decryption computes D(C) ⊕ C′ for the target block, you're choosing C′ to steer the *decrypted* byte until the oracle reports valid padding — at which point you know D(C)[k] ⊕ C′[k] = pad_value, which gives you D(C)[k] directly. Peel the block right-to-left, padding value 0x01, then 0x02, 0x02, then 0x03,0x03,0x03…, fixing already-recovered intermediate bytes with ⊕ pad at each step so the trailing bytes stay valid while you brute-force the next one:

```python def decrypt_block(C, oracle, bs=16): I = bytearray(bs) # intermediate value D(C) for pad in range(1, bs + 1): prefix = bytes(bs - pad) found = False for g in range(256): forged = prefix + bytes([g]) + bytes( I[bs - pad + 1 + i] ^ pad for i in range(pad - 1) ) if oracle(forged + C): if pad == 1: # guard: a "hit" at pad=1 can be a false positive if the # byte you're testing accidentally makes the LAST TWO # bytes read as valid 0x02 0x02 padding instead of a # true 0x01. Flip the second-to-last forged byte and # requery: a real 0x01 hit survives, a 0x02 0x02 # coincidence does not. t = bytearray(forged); t[-2] ^= 1 if not oracle(bytes(t) + C): continue I[bs - pad] = g ^ pad found = True break assert found, "oracle not separable at this position — see below" return bytes(I)

# plaintext_block = xor(decrypt_block(C, oracle), P) ```

Cost: worst case 256 oracle queries per byte, ~128 on average — so ~16×128 queries per 16-byte block. Threading/batching the queries is what makes this practical against a live service instead of a lab.

Beyond decryption — forge arbitrary ciphertext. Once you have D(C) for a block (from decrypting any block that ends up in that position), you get encryption for free without the key: pick any desired plaintext block, set C′ = D(C) ⊕ desired, and that C′ becomes the new "previous block" that decrypts to exactly what you want when followed by C. Chain this backwards from the last block of a message you build to encrypt whole attacker-chosen plaintexts one block at a time — the oracle turns into a full decrypt+encrypt primitive, not just a leak.

Related building blocks: Block Cipher Modes of Operation — the mode carries the CTF vulnerability, not the primitive for why CBC specifically has this shape, CBC Bit-Flipping — controlled tampering with no oracle needed for the no-oracle-needed cousin (single-bit tamper instead of full recovery), RSA LSB / Parity Oracle Attack — recover the plaintext one bit at a time from a decryption oracle for the same one-bit-per-query pattern applied to RSA, ECB Detection & Cut-and-Paste — the identical-block signal and how to weaponize it for the other mode-level tell to check first.

What does NOT work

- Treating the first oracle "hit" at `pad = 1` as ground truth. It is ambiguous by construction: a wrong guess can accidentally produce ... 0x02 0x02 at the tail instead of the true ... 0x01, and the oracle will say "valid" either way. Skip the guard step and you get silently wrong bytes that cascade — every subsequent pad value in that block computes from the corrupted intermediate byte. The guard (flip the second-to-last forged byte and requery) is not optional, it's the difference between a working exploit and one that returns garbage roughly 1/256 of the time per block, which is enough to poison entire runs. - Assuming the wrong block size. A forged block built for 16 bytes against an 8-byte-block cipher (or vice versa) desyncs the whole byte-position arithmetic; the loop still runs and still gets *some* oracle answers, so the failure isn't loud — it just produces wrong plaintext. Verify block size from ciphertext length before trusting output. - A single timing sample as the oracle. If the only distinguishable signal is latency (no separate error message/status code), one measurement is not enough to separate "valid padding" from "invalid" reliably — network and server jitter swamp the real gap. Same fix as any timing side-channel: take many samples and compare medians, not single shots (see Timing Attack — recovering a secret from how long a secret-dependent operation takes) before wiring it into the byte-recovery loop; otherwise the oracle itself is noisy and every layer built on top of it (which assumes a clean boolean) inherits that noise as wrong bytes. - Assuming any decrypt-with-error endpoint is exploitable. The oracle has to leak specifically on the padding-validity boundary. If padding is checked using a constant-time comparison and folded into the *same* generic error as every other failure (or if an integrity check — MAC/AEAD tag — is verified and rejected *before* the padding is ever inspected), there is no distinguishable "padding invalid" class to query, and this attack doesn't get off the ground. Confirm the two classes are genuinely separable — with a deliberately malformed vs. deliberately valid ciphertext — before investing in automating the loop.

Related

Block Cipher Modes of Operation — the mode carries the CTF vulnerability, not the primitive CBC Bit-Flipping — controlled tampering with no oracle needed RSA LSB / Parity Oracle Attack — recover the plaintext one bit at a time from a decryption oracle ECB Detection & Cut-and-Paste — the identical-block signal and how to weaponize it Error / Boolean Oracle Attack — turn any distinguishable valid/invalid response into a key or plaintext Timing Attack — recovering a secret from how long a secret-dependent operation takes

Verified against

18 claims checked against these sources

Source: Sinapsi — verified compositional memory, queryable by LLMs. Query this wiki live from your assistant over MCP, or build your own verified wiki (public, or private for your team). CC BY 4.0 — reuse with attribution to Sinapsi.