Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
Block ciphers like AES only define how to transform a single fixed-size block (16 bytes for AES) under a key. Everything about encrypting a real message — how blocks chain together, how randomness is introduced, how integrity is protected — comes from the mode of operation wrapped around the cipher. Get the mode wrong and AES-256 with a perfect key offers close to no confidentiality at all: the two most common mistakes are using ECB (Electronic Codebook) mode, which encrypts each block independently, and using CBC (Cipher Block Chaining) mode with a static or predictable IV, which silently degrades to ECB-like behavior for repeated prefixes.
These are not exotic bugs. ECB usage shows up constantly in “encrypt this field” library defaults (Java’s Cipher.getInstance("AES") defaults to AES/ECB/PKCS5Padding), and static IVs show up whenever a developer hard-codes an IV, derives it deterministically from non-varying data, or reuses a Cipher object across multiple encryptions. Both mistakes leak structural information about the plaintext — sometimes enough to fully recover it — even though the underlying cipher is never broken.
Attack Prerequisites
Exploiting ECB or static-IV CBC generally requires:
- Access to ciphertext produced by the vulnerable scheme — an encrypted database column, an encrypted cookie/token, an encrypted file or image.
- Structured or repeating plaintext, or the ability to submit attacker-chosen plaintext that gets encrypted (a classic ECB byte-at-a-time decryption oracle needs exactly this).
- Multiple ciphertexts under the same key, especially for static-IV CBC — the weakness appears when comparing ciphertexts across records or requests, not from a single isolated blob.
- No integrity/authentication layer on top, so tampering or pattern analysis is not caught before it can be leveraged.
How It Works
ECB mode computes C[i] = E(K, P[i]) independently for every block — there is no chaining and no IV. Identical plaintext blocks always produce identical ciphertext blocks under the same key. This means any structure in the plaintext (repeated headers, padding, the flat color regions of an image, repeated form fields) is visible directly in the ciphertext’s block pattern. The canonical demonstration is ECB-encrypting a bitmap image: the encrypted output still visibly shows the original picture’s outlines because same-color blocks encrypt to the same ciphertext blocks.
CBC mode fixes the independence problem by XORing each plaintext block with the *previous ciphertext block* before encryption: C[i] = E(K, P[i] XOR C[i-1]), with C[0] replaced by a random IV for the first block. The IV’s job is to randomize that first block so that two messages sharing an identical prefix still diverge from block one. If the IV is static (hard-coded, zeroed, or otherwise reused across encryptions) or predictable (e.g. a counter, a timestamp, or derived from public data), two plaintexts with the same first block encrypt to the same first ciphertext block again — the scheme degenerates to leaking equality of prefixes exactly like ECB, and an attacker who can submit chosen plaintext can use this to confirm guesses about a victim’s secret data block by block.
A related and equally common failure is IV reuse combined with a known or chosen-plaintext oracle: if an attacker can get the application to encrypt attacker-chosen data alongside a secret (e.g. AES-CBC-Encrypt(secret + attacker_input) with the same key and IV each call), they can perform a byte-at-a-time recovery of the secret by observing which chosen prefix causes a ciphertext block collision with the block containing the unknown secret byte — the CBC analogue of the classic ECB byte-at-a-time decryption technique.
Vulnerable Code / Configuration
The single most common Java mistake — relying on the JCE default cipher transformation, which is ECB:
// VULNERABLE: "AES" resolves to AES/ECB/PKCS5Padding on most JCE providers
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] ciphertext = cipher.doFinal(creditCardNumber.getBytes());
// Every customer whose card number starts with the same BIN now shares
// identical leading ciphertext blocks.
JavaA static or all-zero IV in CBC mode, frequently introduced by copy-pasted sample code or a misunderstanding of what the IV parameter is for:
from Crypto.Cipher import AES
STATIC_IV = b'\x00' * 16 # VULNERABLE: same IV every call
def encrypt_field(plaintext, key):
cipher = AES.new(key, AES.MODE_CBC, STATIC_IV)
return cipher.encrypt(pad(plaintext, 16))
# Two rows with the same first 16 bytes of plaintext (e.g. "SSN: 078-05-")
# now produce identical first ciphertext blocks, revealing the match.
PythonA related config-level version of the same bug: a .env/config file that pins the IV as a constant alongside the key, rather than generating a fresh random IV per encryption and prepending it to the ciphertext:
# config.yml — VULNERABLE
encryption:
algorithm: AES-256-CBC
key: ${ENC_KEY}
iv: "000102030405060708090a0b0c0d0e0f" # fixed, checked into source control
TEXTWalkthrough / Exploitation
Detecting ECB is often as simple as inspecting ciphertext for repeated 16-byte blocks. Given a hex-encoded ciphertext, a quick check:
def looks_like_ecb(ciphertext_hex, block_size=16):
raw = bytes.fromhex(ciphertext_hex)
blocks = [raw[i:i+block_size] for i in range(0, len(raw), block_size)]
return len(blocks) != len(set(blocks)) # any duplicate block => ECB signal
PythonIf the application exposes an encryption oracle that appends attacker input before a secret (a very common pattern: encrypt(attacker_input + secret_cookie)), the classic ECB byte-at-a-time attack recovers the secret one byte per iteration by aligning a known prefix against a guessed byte:
# classic ECB byte-at-a-time decryption sketch
def recover_secret(oracle, block_size=16):
known = b''
for i in range(SECRET_LEN):
pad_len = block_size - 1 - (i % block_size)
filler = b'A' * pad_len
target_block_idx = (len(filler) + i) // block_size
target = oracle(filler)[target_block_idx*block_size:(target_block_idx+1)*block_size]
for guess in range(256):
trial = filler + known + bytes([guess])
trial_block = oracle(trial)[target_block_idx*block_size:(target_block_idx+1)*block_size]
if trial_block == target:
known += bytes([guess])
break
return known
PythonFor static-IV CBC, the equivalent proof of concept is simpler: encrypt two records with a known and an unknown value sharing a candidate prefix and compare first-block ciphertext equality — a match confirms the guessed prefix, letting an attacker binary-search or dictionary-attack sensitive fixed-format fields (SSNs, account numbers) without ever recovering the key.
Note: ‘AES-256’ in a security questionnaire or compliance doc says nothing about safety — always ask for the *mode* and *IV handling*. ECB should be treated as an automatic fail in code review; a static/reused IV in CBC is nearly as bad and is easy to miss because the code compiles and ‘works’ functionally.
Opsec: When reviewing a black-box API, request the same plaintext-like input twice (e.g. two accounts with the same starting substring) and diff the returned ciphertext at the block level; identical leading blocks across unrelated requests is a strong ECB/static-IV signal without needing source access.
Detection and Defense
The fix is to stop using ECB and to make IVs unpredictable and single-use:
- Never use ECB for anything beyond single, independent, already-random blocks (e.g. wrapping a symmetric key). Explicitly specify the mode — never rely on a library’s bare
"AES"default. - Generate a fresh, cryptographically random IV for every CBC encryption (
os.urandom(16)/SecureRandom), and prepend it to the ciphertext so the receiver can recover it — the IV does not need to be secret, only unique and unpredictable. - Prefer AEAD modes (AES-GCM, ChaCha20-Poly1305) over bare CBC; they fold in a nonce and integrity tag correctly by construction and remove an entire class of mode-misuse bugs.
- Never reuse a nonce/IV with the same key in GCM either — nonce reuse under GCM is catastrophic (full authentication-key recovery), so if IVs are static-config today, migrating naively to GCM without fixing IV generation makes things worse, not better.
- Grep source and dependency defaults for
ECB, hard-coded IV byte arrays, andCipher.getInstance("AES")/"DES"/"Blowfish"without an explicit mode as part of secure code review and SAST rules.
Real-World Impact
ECB-mode misuse is a textbook finding repeatedly rediscovered in mobile apps and legacy enterprise software that encrypt local data or API payloads with AES/ECB/PKCS5Padding, and it is the standard classroom example (the ‘ECB penguin’) for demonstrating that encryption without proper chaining leaks plaintext structure. Static-IV CBC has been repeatedly found in vendor products’ “encrypted” configuration backups and session tokens during security assessments, where identical IV/key pairs across devices or deployments allowed cross-tenant plaintext comparison. Both issues are routine, high-confidence findings in application penetration tests and cryptographic code reviews precisely because they compile, function correctly, and pass casual QA while providing far weaker confidentiality than the algorithm name implies.
Conclusion
AES is not a promise of security by itself — the mode of operation and IV handling around it determine whether ciphertext actually hides plaintext structure. ECB should never appear in new code, and CBC is only as strong as its IV is random and unique per message. When in doubt, reach for an AEAD mode with a properly generated nonce and let the library handle chaining and integrity correctly rather than hand-rolling either.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments