Browse CTFs New CTF Sign in

Exploiting Symmetric Key Reuse Across Users: Cross-Account Ciphertext Oracle Attacks

cloud_container_security Difficulty 1–5 30 min certifiable

Theory

Why This Matters

In 2017, researchers auditing a widely deployed SaaS identity platform discovered that user profile data was encrypted with a single global AES key without per-user key derivation or unique IVs. Because the platform used ECB mode, two users whose profile fields happened to contain identical values produced identical ciphertext blocks — enabling cross-account data comparison and, for fields with a small value space (e.g., account status flags), direct enumeration of plaintext values. A structurally similar issue was disclosed in 2019 in a mobile banking application where all JWT-like token payloads were AES-CBC-encrypted with a fixed global key and a constant IV, meaning the first block of any two tokens with the same account type prefix was identical, leaking account category. These incidents illustrate that symmetric key reuse across users is not merely a theoretical concern — it manifests as information leakage and, in some configurations, as privilege escalation.

Core Concept

Symmetric key reuse across users occurs when a single AES (or other symmetric cipher) key encrypts data belonging to multiple users without per-user cryptographic isolation. The attack surface depends on the mode of operation:

In ECB mode, the absence of chaining means that identical plaintext blocks always produce identical ciphertext blocks, regardless of which user the data belongs to. An attacker with access to two users' encrypted records can identify which fields have equal values by comparing ciphertext blocks directly — without knowing the key or the plaintext. This enables cross-user data comparison, plaintext enumeration for low-cardinality fields, and block replay: copying a ciphertext block from one user's record into another user's record to substitute the corresponding field value.

In CBC mode with a fixed IV, only the first block is deterministically encrypted — subsequent blocks are randomised by chaining. This limits leakage to the first block. If the first plaintext block contains a predictable prefix (e.g., a fixed JSON header {"role": or a user ID format), an attacker can identify users with identical role prefixes by comparing their first ciphertext blocks.

In CBC mode with a random per-message IV, key reuse alone does not directly enable block comparison between users. However, if the attacker can control partial plaintext (e.g., a username or address field), they can mount a chosen-plaintext attack: by crafting their own data to match a target user's prefix, they can determine whether the target's plaintext matches their crafted input.

The cryptanalytic preconditions vary: for ECB-based leakage, ciphertext-only access to multiple users' encrypted data suffices. For CBC fixed-IV leakage, ciphertext-only access to the first block is sufficient. For CBC random-IV chosen-plaintext comparison, the attacker needs write access to their own account and read access to another account's ciphertext.

The correct architectural defence is per-user key derivation: each user's data key is derived from a master key and a user-unique identifier using HKDF (HMAC-based Key Derivation Function), ensuring that even if one user's key is compromised (or their ciphertext is analysed), no information is leaked about other users' data.

Technical Deep-Dive

from Crypto.Cipher import AES
import os

BLOCK = 16

def detect_ecb_cross_user(ciphertexts: list[bytes]) -> list[tuple[int,int,int]]:
    """
    Find identical 16-byte blocks across different users' ciphertexts.
    Returns list of (user_i, user_j, block_offset) collisions.
    """
    block_map = {}  # block_bytes -> (user_idx, block_idx)
    collisions = []
    for user_idx, ct in enumerate(ciphertexts):
        for blk_idx in range(0, len(ct), BLOCK):
            block = ct[blk_idx:blk_idx+BLOCK]
            if len(block) < BLOCK:
                continue
            key = block
            if key in block_map:
                prev_user, prev_blk = block_map[key]
                if prev_user != user_idx:
                    collisions.append((prev_user, user_idx, blk_idx))
            else:
                block_map[key] = (user_idx, blk_idx)
    return collisions

def detect_cbc_fixed_iv_leak(ciphertexts: list[bytes]) -> list[tuple[int,int]]:
    """
    Detect users whose first ciphertext block is identical,
    indicating their first plaintext block matches (CBC with fixed IV).
    """
    first_blocks = {}
    leaks = []
    for user_idx, ct in enumerate(ciphertexts):
        fb = ct[:BLOCK]
        if fb in first_blocks:
            leaks.append((first_blocks[fb], user_idx))
        else:
            first_blocks[fb] = user_idx
    return leaks

# HKDF-based per-user key derivation (correct approach)
import hmac, hashlib

def derive_user_key(master_key: bytes, user_id: str, length: int = 32) -> bytes:
    """
    HKDF-Extract + HKDF-Expand for per-user AES key.
    RFC 5869 compliant.
    """
    # Extract: PRK = HMAC-SHA256(salt=master_key, IKM=user_id)
    prk = hmac.new(master_key, user_id.encode(), hashlib.sha256).digest()
    # Expand: OKM = first 'length' bytes of T(1) = HMAC(PRK, info || 0x01)
    info = b"user-data-key"
    t1 = hmac.new(prk, info + b"x01", hashlib.sha256).digest()
    return t1[:length]

Cryptanalysis Methodology

  1. Collect multi-user ciphertext samples — Gather encrypted data for as many user accounts as possible. In a web application, this may mean creating multiple test accounts and collecting their encrypted profile fields, cookies, or tokens.
  2. Identify the cipher mode — Attempt ECB detection: register two accounts with identical field values. If any ciphertext blocks match, ECB mode is confirmed. For CBC, note whether the IV is included in the transmitted ciphertext (usually prepended as the first 16 bytes).
  3. Run cross-user block comparison — Apply detect_ecb_cross_user() to the collected ciphertext set. Note which block positions collide between users; these positions correspond to fields with equal plaintext values.
  4. Enumerate field values — For fields with a small value space (e.g., role: "admin"/"user"/"moderator", subscription: "free"/"premium"), create accounts with each known value and collect the resulting ciphertext block fingerprint. Build a ciphertext-to-plaintext lookup table for those blocks.
  5. Attempt block replay — In ECB mode, copy a ciphertext block from one user's record (e.g., the "role" block for an admin account) and substitute it into another user's ciphertext. Submit the modified ciphertext to the application. If it is accepted, privilege escalation is confirmed.
  6. Test CBC fixed-IV first-block leak — Apply detect_cbc_fixed_iv_leak(). If the first block collides between users with the same account type, the IV is fixed and the first plaintext block leaks.
  7. Report and scope impact — Document which fields leak, whether privilege escalation via block replay is possible, and the scale of user data exposure.

Secure Implementation Note — Derive a unique per-user key using HKDF (RFC 5869) with a high-entropy master key and the user's unique identifier as the derivation context. Use AES-256-GCM with a random 96-bit nonce per encryption operation. Never reuse the same key and nonce pair across different users or different messages.

Common Cryptanalysis Errors

  • Testing only ECB and ignoring CBC fixed-IV leakage — Analysts who rule out ECB (because blocks within a single ciphertext do not repeat) may miss CBC fixed-IV leakage across users. Always test both cases.
  • Not creating enough test accounts — With only two test accounts, statistical collisions are unlikely unless fields are deliberately set equal. Create at least five to ten accounts with varied field values to build a comprehensive block fingerprint map.
  • Assuming key reuse implies mode vulnerability — CBC with a random IV per message and a global key is significantly harder to exploit than ECB. Do not report key reuse as critical without demonstrating concrete plaintext leakage or block replay.
  • Forgetting encoding layers — Ciphertexts are often base64 or hex-encoded in transmission. Decode to raw bytes before performing block comparison; comparing encoded strings will miss all collisions.
  • Confusing block-level collisions with full message collisions — Two ciphertexts that are identical in total length and have matching first blocks may differ in other blocks. Report block-level granularity, not message-level.
  • Missing HMAC-keyed comparison as an alternative — Some applications compute HMAC(key, user_data) as a "fingerprint" rather than encrypting. This produces a deterministic tag; if the HMAC key is global and the input includes user-controlled data, the fingerprint leaks equality without revealing the HMAC key.

NICE Framework Alignment

Code Knowledge/Skill/Task Statement How This Card Develops It
K0018 Knowledge of encryption algorithms Identifies how ECB mode and CBC with fixed IV create cross-user information leakage under key reuse
K0019 Knowledge of cryptography and cryptographic key management concepts Motivates per-user key derivation via HKDF as the correct key management pattern
K0305 Knowledge of data concealment Shows that "encrypted" data under a shared key is not isolated between users without per-user key diversification
S0138 Skill in using public-key encryption and PKI Develops systematic multi-user ciphertext analysis as a structured assessment technique
T0259 Conduct vulnerability assessment using established frameworks Trains detection of key reuse patterns in multi-tenant application cryptography assessments

Further Reading

  • HKDF: A Cryptographic Key Derivation Function — Hugo Krawczyk and Pasi Eronen, RFC 5869, IETF 2010
  • Cryptographic Key Management Workshop Summary — NIST SP 800-130, 2013
  • Practical Cryptography (Chapter 10: Key Management) — Niels Ferguson and Bruce Schneier, Wiley, 2003

Challenge Lab

Reinforce your learning with a hands-on generated challenge based on this card's competency.