ctf-agent 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +131 -0
- package/Dockerfile +192 -0
- package/README.md +522 -0
- package/agents/ctf-analyzer.md +58 -0
- package/agents/ctf-controller.md +151 -0
- package/agents/ctf-speedrun.md +74 -0
- package/bin/ctf-agent.js +119 -0
- package/docker-compose.yml +13 -0
- package/mcp_config.json +19 -0
- package/package.json +44 -0
- package/references/ctf-triage-ladder.md +68 -0
- package/references/exploit-databases.md +575 -0
- package/references/llm-safety-and-policy-compliance.md +173 -0
- package/references/multi-agent-orchestration-and-policy-routing.md +278 -0
- package/references/security-events-and-intelligence.md +566 -0
- package/references/version-matrix.md +59 -0
- package/rules/ctf-execution-rules.md +44 -0
- package/rules/ctf-safety-framing-rules.md +140 -0
- package/rules/flag-validation-rules.md +25 -0
- package/scripts/__init__.py +1 -0
- package/scripts/ctf-tools.lock +205 -0
- package/scripts/ctf_init.py +1026 -0
- package/scripts/ctf_update.py +583 -0
- package/scripts/ctfd_client.py +81 -0
- package/scripts/cve_lookup.py +453 -0
- package/scripts/extract_flags.py +155 -0
- package/scripts/install_as_agent.py +278 -0
- package/scripts/install_ctf_tools.sh +603 -0
- package/scripts/parallel_triage.py +395 -0
- package/scripts/prompt_policy_sanitizer.py +283 -0
- package/scripts/scope_guard.py +444 -0
- package/scripts/skill_validator.py +504 -0
- package/scripts/workspace_cleaner.py +141 -0
- package/skills/ctf-ai-ml/SKILL.md +120 -0
- package/skills/ctf-ai-ml/adversarial-ml.md +623 -0
- package/skills/ctf-ai-ml/llm-attacks.md +487 -0
- package/skills/ctf-ai-ml/model-attacks.md +422 -0
- package/skills/ctf-crypto/SKILL.md +320 -0
- package/skills/ctf-crypto/advanced-math.md +798 -0
- package/skills/ctf-crypto/classic-ciphers.md +651 -0
- package/skills/ctf-crypto/ecc-attacks.md +347 -0
- package/skills/ctf-crypto/exotic-crypto-2.md +380 -0
- package/skills/ctf-crypto/exotic-crypto.md +528 -0
- package/skills/ctf-crypto/historical.md +113 -0
- package/skills/ctf-crypto/lattice-and-lwe.md +524 -0
- package/skills/ctf-crypto/modern-ciphers-2.md +563 -0
- package/skills/ctf-crypto/modern-ciphers-3.md +453 -0
- package/skills/ctf-crypto/modern-ciphers.md +649 -0
- package/skills/ctf-crypto/prng-attacks.md +257 -0
- package/skills/ctf-crypto/prng.md +664 -0
- package/skills/ctf-crypto/rsa-attacks-2.md +792 -0
- package/skills/ctf-crypto/rsa-attacks.md +487 -0
- package/skills/ctf-crypto/stream-ciphers.md +390 -0
- package/skills/ctf-crypto/zkp-and-advanced.md +456 -0
- package/skills/ctf-forensics/3d-printing.md +121 -0
- package/skills/ctf-forensics/SKILL.md +379 -0
- package/skills/ctf-forensics/disk-advanced.md +497 -0
- package/skills/ctf-forensics/disk-and-memory.md +491 -0
- package/skills/ctf-forensics/disk-recovery.md +699 -0
- package/skills/ctf-forensics/linux-forensics.md +511 -0
- package/skills/ctf-forensics/network-advanced.md +583 -0
- package/skills/ctf-forensics/network.md +645 -0
- package/skills/ctf-forensics/peripheral-capture.md +287 -0
- package/skills/ctf-forensics/signals-and-hardware.md +713 -0
- package/skills/ctf-forensics/steganography.md +694 -0
- package/skills/ctf-forensics/stego-advanced-2.md +475 -0
- package/skills/ctf-forensics/stego-advanced.md +481 -0
- package/skills/ctf-forensics/stego-image.md +691 -0
- package/skills/ctf-forensics/windows.md +625 -0
- package/skills/ctf-malware/SKILL.md +181 -0
- package/skills/ctf-malware/c2-and-protocols.md +274 -0
- package/skills/ctf-malware/pe-and-dotnet.md +108 -0
- package/skills/ctf-malware/scripts-and-obfuscation.md +449 -0
- package/skills/ctf-misc/SKILL.md +498 -0
- package/skills/ctf-misc/bashjails.md +323 -0
- package/skills/ctf-misc/ctfd-navigation.md +465 -0
- package/skills/ctf-misc/dns.md +255 -0
- package/skills/ctf-misc/encodings-advanced.md +504 -0
- package/skills/ctf-misc/encodings.md +431 -0
- package/skills/ctf-misc/games-and-vms-2.md +254 -0
- package/skills/ctf-misc/games-and-vms-3.md +690 -0
- package/skills/ctf-misc/games-and-vms-4.md +229 -0
- package/skills/ctf-misc/games-and-vms.md +529 -0
- package/skills/ctf-misc/linux-privesc.md +333 -0
- package/skills/ctf-misc/pyjails.md +671 -0
- package/skills/ctf-misc/rf-sdr.md +91 -0
- package/skills/ctf-osint/SKILL.md +198 -0
- package/skills/ctf-osint/geolocation-and-media.md +464 -0
- package/skills/ctf-osint/social-media.md +312 -0
- package/skills/ctf-osint/web-and-dns.md +341 -0
- package/skills/ctf-pwn/SKILL.md +214 -0
- package/skills/ctf-pwn/advanced-exploits-2.md +579 -0
- package/skills/ctf-pwn/advanced-exploits-3.md +598 -0
- package/skills/ctf-pwn/advanced-exploits-4.md +590 -0
- package/skills/ctf-pwn/advanced-exploits-5.md +119 -0
- package/skills/ctf-pwn/advanced-exploits.md +773 -0
- package/skills/ctf-pwn/advanced.md +326 -0
- package/skills/ctf-pwn/field-notes.md +245 -0
- package/skills/ctf-pwn/format-string.md +694 -0
- package/skills/ctf-pwn/heap-fsop.md +285 -0
- package/skills/ctf-pwn/heap-techniques-2.md +333 -0
- package/skills/ctf-pwn/heap-techniques.md +513 -0
- package/skills/ctf-pwn/kernel-bypass.md +421 -0
- package/skills/ctf-pwn/kernel-techniques.md +366 -0
- package/skills/ctf-pwn/kernel.md +636 -0
- package/skills/ctf-pwn/overflow-basics.md +611 -0
- package/skills/ctf-pwn/rop-advanced.md +725 -0
- package/skills/ctf-pwn/rop-and-shellcode.md +659 -0
- package/skills/ctf-pwn/sandbox-escape.md +313 -0
- package/skills/ctf-reverse/SKILL.md +163 -0
- package/skills/ctf-reverse/anti-analysis-ctf.md +204 -0
- package/skills/ctf-reverse/anti-analysis.md +693 -0
- package/skills/ctf-reverse/field-notes.md +376 -0
- package/skills/ctf-reverse/languages-compiled.md +666 -0
- package/skills/ctf-reverse/languages-platforms.md +592 -0
- package/skills/ctf-reverse/languages.md +553 -0
- package/skills/ctf-reverse/patterns-ctf-2.md +397 -0
- package/skills/ctf-reverse/patterns-ctf-3.md +797 -0
- package/skills/ctf-reverse/patterns-ctf.md +670 -0
- package/skills/ctf-reverse/patterns-runtime.md +274 -0
- package/skills/ctf-reverse/patterns.md +572 -0
- package/skills/ctf-reverse/platforms-hardware.md +387 -0
- package/skills/ctf-reverse/platforms.md +664 -0
- package/skills/ctf-reverse/tools-advanced-2.md +421 -0
- package/skills/ctf-reverse/tools-advanced.md +407 -0
- package/skills/ctf-reverse/tools-dynamic.md +679 -0
- package/skills/ctf-reverse/tools-emulation.md +319 -0
- package/skills/ctf-reverse/tools.md +573 -0
- package/skills/ctf-web/SKILL.md +153 -0
- package/skills/ctf-web/auth-and-access-2.md +82 -0
- package/skills/ctf-web/auth-and-access.md +783 -0
- package/skills/ctf-web/auth-infra.md +321 -0
- package/skills/ctf-web/auth-jwt.md +186 -0
- package/skills/ctf-web/client-side-advanced.md +739 -0
- package/skills/ctf-web/client-side.md +529 -0
- package/skills/ctf-web/cves.md +373 -0
- package/skills/ctf-web/field-notes.md +482 -0
- package/skills/ctf-web/node-and-prototype.md +200 -0
- package/skills/ctf-web/server-side-2.md +337 -0
- package/skills/ctf-web/server-side-advanced-2.md +559 -0
- package/skills/ctf-web/server-side-advanced-3.md +125 -0
- package/skills/ctf-web/server-side-advanced-4.md +480 -0
- package/skills/ctf-web/server-side-advanced.md +378 -0
- package/skills/ctf-web/server-side-deser.md +443 -0
- package/skills/ctf-web/server-side-exec-2.md +799 -0
- package/skills/ctf-web/server-side-exec.md +457 -0
- package/skills/ctf-web/server-side.md +629 -0
- package/skills/ctf-web/sql-injection.md +790 -0
- package/skills/ctf-web/web3.md +374 -0
- package/skills/ctf-writeup/SKILL.md +90 -0
- package/skills/solve-challenge/SKILL.md +269 -0
- package/skills.json +16 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
# CTF Crypto - Modern Cipher Attacks (Continued)
|
|
2
|
+
|
|
3
|
+
Hash-based attacks, protocol-level exploits, ECB oracles, Rabin/RSA parity attacks, and specialized cipher weaknesses. For core AES/CBC/padding oracle techniques, see [modern-ciphers.md](modern-ciphers.md). For stream cipher attacks (LFSR, RC4, XOR), see [stream-ciphers.md](stream-ciphers.md).
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [Blum-Goldwasser Bit-Extension Oracle (PlaidCTF 2013)](#blum-goldwasser-bit-extension-oracle-plaidctf-2013)
|
|
7
|
+
- [Hash Length Extension Attack (PlaidCTF 2014)](#hash-length-extension-attack-plaidctf-2014)
|
|
8
|
+
- [Compression Oracle / CRIME-Style Attack (BCTF 2015)](#compression-oracle--crime-style-attack-bctf-2015)
|
|
9
|
+
- [Hash Function Time Reversal via Cycle Detection (BSidesSF 2025)](#hash-function-time-reversal-via-cycle-detection-bsidessf-2025)
|
|
10
|
+
- [OFB Mode with Invertible RNG Backward Decryption (BSidesSF 2026)](#ofb-mode-with-invertible-rng-backward-decryption-bsidessf-2026)
|
|
11
|
+
- [Weak Key Derivation via Public Key Hash XOR (BSidesSF 2026)](#weak-key-derivation-via-public-key-hash-xor-bsidessf-2026)
|
|
12
|
+
- [HMAC-CRC Linearity Attack (Boston Key Party 2016)](#hmac-crc-linearity-attack-boston-key-party-2016)
|
|
13
|
+
- [DES Weak Keys in OFB Mode (Boston Key Party 2016)](#des-weak-keys-in-ofb-mode-boston-key-party-2016)
|
|
14
|
+
- [SRP (Secure Remote Password) Protocol Bypass via Modular Arithmetic (ASIS CTF Finals 2016)](#srp-secure-remote-password-protocol-bypass-via-modular-arithmetic-asis-ctf-finals-2016)
|
|
15
|
+
- [Modified AES S-Box Brute-Force Recovery (H4ckIT CTF 2016)](#modified-aes-s-box-brute-force-recovery-h4ckit-ctf-2016)
|
|
16
|
+
- [Square Attack on Reduced-Round AES (0CTF 2016)](#square-attack-on-reduced-round-aes-0ctf-2016)
|
|
17
|
+
- [AES-ECB Byte-at-a-Time Chosen Plaintext (ABCTF 2016)](#aes-ecb-byte-at-a-time-chosen-plaintext-abctf-2016)
|
|
18
|
+
- [AES-ECB Cut-and-Paste Block Manipulation (NDH Quals 2016)](#aes-ecb-cut-and-paste-block-manipulation-ndh-quals-2016)
|
|
19
|
+
- [AES-CBC IV Bit-Flip Authentication Bypass (Google CTF 2016)](#aes-cbc-iv-bit-flip-authentication-bypass-google-ctf-2016)
|
|
20
|
+
- [Rabin Cryptosystem LSB Parity Oracle (PlaidCTF 2016)](#rabin-cryptosystem-lsb-parity-oracle-plaidctf-2016)
|
|
21
|
+
- [PBKDF2 Pre-Hash Bypass for Long Passwords (BackdoorCTF 2016)](#pbkdf2-pre-hash-bypass-for-long-passwords-backdoorctf-2016)
|
|
22
|
+
- [MD5 Multi-Collision via Fastcol (BackdoorCTF 2016)](#md5-multi-collision-via-fastcol-backdoorctf-2016)
|
|
23
|
+
- [GHASH Key Recovery over Prime Modulus (nullcon HackIM 2019)](#ghash-key-recovery-over-prime-modulus-nullcon-hackim-2019)
|
|
24
|
+
- [SHA-1 Length Extension Plus AES-CBC Cookie Forgery (BSidesSF 2019)](#sha-1-length-extension-plus-aes-cbc-cookie-forgery-bsidessf-2019)
|
|
25
|
+
|
|
26
|
+
See [modern-ciphers-3.md](modern-ciphers-3.md) for custom hash reversal, CRC32 brute-force, noisy RSA oracle, sponge collisions, CBC IV forgery, padding oracle bit-flip, SPN S-box intersection, AES-CFB IV recovery, three-round XOR, Unicode side channel, SHA-256 basis attack, and HMAC key recovery.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Blum-Goldwasser Bit-Extension Oracle (PlaidCTF 2013)
|
|
31
|
+
|
|
32
|
+
**Pattern:** Exploit a decryption oracle for Blum-Goldwasser-style encryption by extending ciphertext length by one bit per query to leak plaintext via parity.
|
|
33
|
+
|
|
34
|
+
**Key insight:** Extend ciphertext by one bit (L+1), shift ciphertext left (`c << 1`), and submit a modified `y` value. The oracle reveals the LSB (parity) of each decrypted chunk. The squaring sequence `y = pow(y, 2, N)` can be manipulated to produce valid extended ciphertexts the server hasn't seen.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
# Iterative plaintext recovery via bit-extension
|
|
38
|
+
for i in range(msg_length):
|
|
39
|
+
extended_c = original_c << 1 # Shift ciphertext left by 1
|
|
40
|
+
new_y = pow(original_y, 2, N) # Advance squaring sequence
|
|
41
|
+
response = oracle(extended_c, new_y, msg_length + 1)
|
|
42
|
+
leaked_bit = response & 1 # LSB reveals one plaintext bit
|
|
43
|
+
plaintext_bits.append(leaked_bit)
|
|
44
|
+
original_y = new_y
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**When to use:** Blum-Goldwasser or BBS-based (Blum Blum Shub) encryption with a decryption oracle that accepts variable-length ciphertexts. The parity leak accumulates one bit per query.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Hash Length Extension Attack (PlaidCTF 2014)
|
|
52
|
+
|
|
53
|
+
**Pattern:** Server computes `hash(SECRET || user_data)` using MD5, SHA-1, or SHA-256 (Merkle-Damgard constructions). Given a valid hash and the original data, extend it with arbitrary appended data and compute a valid hash — without knowing the secret.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Using HashPump (install: apt install hashpump)
|
|
57
|
+
hashpump --keylength 8 \
|
|
58
|
+
--signature 'ef16c2bffbcf0b7567217f292f9c2a9a50885e01e002fa34db34c0bb916ed5c3' \
|
|
59
|
+
--data 'original_data' \
|
|
60
|
+
--additional ';admin=true'
|
|
61
|
+
# Outputs: new_signature and new_data (with padding bytes)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
# Python: hashpumpy
|
|
66
|
+
import hashpumpy
|
|
67
|
+
new_hash, new_data = hashpumpy.hashpump(
|
|
68
|
+
original_hash, original_data, append_data, secret_length
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Key insight:** Merkle-Damgard hashes (MD5, SHA-1, SHA-256) process data in blocks, and the hash output IS the internal state. Given `H(secret || msg)`, you can compute `H(secret || msg || padding || extension)` without knowing `secret` — just initialize the hash state from the known output and continue hashing. Only HMAC (`H(K XOR opad || H(K XOR ipad || msg))`) is immune. If the secret length is unknown, try lengths 1-32.
|
|
73
|
+
|
|
74
|
+
*See also [ctf-web/auth-infra.md — Hash Length Extension Attack (ASIS CTF 2017)](../ctf-web/auth-infra.md#hash-length-extension-attack-asis-ctf-2017) for the same primitive applied to a web auth token bypass.*
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Compression Oracle / CRIME-Style Attack (BCTF 2015)
|
|
79
|
+
|
|
80
|
+
**Pattern:** Server compresses plaintext (LZW, zlib, etc.) before encrypting. By observing ciphertext length changes with chosen plaintexts, leak the unknown plaintext character-by-character.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import base64
|
|
84
|
+
|
|
85
|
+
def oracle(plaintext):
|
|
86
|
+
"""Send chosen plaintext, get ciphertext length."""
|
|
87
|
+
resp = send_to_server(plaintext)
|
|
88
|
+
return len(base64.b64decode(resp))
|
|
89
|
+
|
|
90
|
+
# Baseline: empty input
|
|
91
|
+
base_len = oracle("")
|
|
92
|
+
|
|
93
|
+
# Recover secret byte-by-byte
|
|
94
|
+
known = ""
|
|
95
|
+
for pos in range(secret_length):
|
|
96
|
+
for c in string.printable:
|
|
97
|
+
candidate = known + c
|
|
98
|
+
length = oracle(candidate)
|
|
99
|
+
if length <= base_len + len(known): # Compressed = match
|
|
100
|
+
known += c
|
|
101
|
+
break
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Key insight:** Compression algorithms (LZW, DEFLATE, zlib) replace repeated sequences with back-references. If `SALT + user_input` is compressed before encryption, sending input that matches part of the salt produces shorter ciphertext (the match compresses). This is the same class as CRIME (TLS), BREACH (HTTP), and HEIST attacks. The oracle is ciphertext length.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Hash Function Time Reversal via Cycle Detection (BSidesSF 2025)
|
|
109
|
+
|
|
110
|
+
When a system uses iterated hashing as a "time" function (`state_t = H(state_{t-1})`), reverse time by exploiting the finite cycle structure:
|
|
111
|
+
|
|
112
|
+
1. **Detect cycle:** Use Floyd's tortoise-and-hare or Brent's algorithm to find cycle length L
|
|
113
|
+
2. **Compute backward steps:** To go from time T to earlier time T_goal: iterate forward `(L - (T - T_goal)) % L` steps
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import hashlib
|
|
117
|
+
|
|
118
|
+
def hash_step(state):
|
|
119
|
+
return hashlib.md5(state).digest()[:8] # Truncated hash
|
|
120
|
+
|
|
121
|
+
def find_cycle(start):
|
|
122
|
+
"""Brent's cycle detection: returns (cycle_length, start_of_cycle)"""
|
|
123
|
+
power = lam = 1
|
|
124
|
+
tortoise = start
|
|
125
|
+
hare = hash_step(start)
|
|
126
|
+
while tortoise != hare:
|
|
127
|
+
if power == lam:
|
|
128
|
+
tortoise = hare
|
|
129
|
+
power *= 2
|
|
130
|
+
lam = 0
|
|
131
|
+
hare = hash_step(hare)
|
|
132
|
+
lam += 1
|
|
133
|
+
# lam = cycle length; find cycle start
|
|
134
|
+
tortoise = hare = start
|
|
135
|
+
for _ in range(lam):
|
|
136
|
+
hare = hash_step(hare)
|
|
137
|
+
mu = 0
|
|
138
|
+
while tortoise != hare:
|
|
139
|
+
tortoise = hash_step(tortoise)
|
|
140
|
+
hare = hash_step(hare)
|
|
141
|
+
mu += 1
|
|
142
|
+
return lam, mu # cycle_length, cycle_start_offset
|
|
143
|
+
|
|
144
|
+
# Reverse from T_known to T_goal
|
|
145
|
+
cycle_len, _ = find_cycle(known_state)
|
|
146
|
+
forward_steps = (cycle_len - (t_known - t_goal)) % cycle_len
|
|
147
|
+
state = known_state
|
|
148
|
+
for _ in range(forward_steps):
|
|
149
|
+
state = hash_step(state)
|
|
150
|
+
# state is now the value at t_goal
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Key insight:** For truncated hashes (e.g., MD5 -> 64 bits), the expected cycle length is ~2^32, making cycle detection feasible. Going "backward" N steps is equivalent to going forward (cycle_length - N) steps. Assumes the target state is within the main cycle, not on a tail.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## OFB Mode with Invertible RNG Backward Decryption (BSidesSF 2026)
|
|
158
|
+
|
|
159
|
+
**Pattern (randcrypt):** A custom block cipher uses OFB (Output Feedback) mode with a homemade RNG as the keystream generator. The last plaintext block is known (zero padding), leaking one RNG state. If the RNG's state transition function is invertible (bijective), all previous states can be recovered by running the RNG backwards, decrypting the entire ciphertext from the end to the beginning.
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
def rng_forward(state):
|
|
163
|
+
"""Custom RNG state transition (from challenge)."""
|
|
164
|
+
# Example: linear congruential or reversible mixing
|
|
165
|
+
return (state * A + B) % M
|
|
166
|
+
|
|
167
|
+
def rng_inverse(state):
|
|
168
|
+
"""Inverted RNG — recover previous state."""
|
|
169
|
+
return ((state - B) * pow(A, -1, M)) % M
|
|
170
|
+
|
|
171
|
+
# Last block is zero-padded → ciphertext XOR 0 = keystream = RNG state
|
|
172
|
+
leaked_state = int.from_bytes(ciphertext_blocks[-2], 'big')
|
|
173
|
+
|
|
174
|
+
# Decrypt backwards
|
|
175
|
+
state = leaked_state
|
|
176
|
+
plaintext_blocks = []
|
|
177
|
+
for i in range(len(ciphertext_blocks) - 3, -1, -1):
|
|
178
|
+
state = rng_inverse(state)
|
|
179
|
+
pt = xor_bytes(ciphertext_blocks[i], state.to_bytes(block_size, 'big'))
|
|
180
|
+
plaintext_blocks.insert(0, pt)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Key insight:** OFB mode decouples encryption from the plaintext — the keystream is deterministic from the initial state. If ANY block's plaintext is known (padding, headers, magic bytes), the corresponding RNG state is leaked. An invertible RNG then reveals ALL states. Always check if the RNG transition function has a mathematical inverse.
|
|
184
|
+
|
|
185
|
+
**When to recognize:** Custom OFB/CTR mode with a non-standard PRNG. Look for: (1) XOR-based encryption, (2) a state-update function that's bijective (no information loss), (3) predictable plaintext in any block position. Files with known padding (PKCS#7 zero-fill, null-terminated strings) are ideal leak points.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Weak Key Derivation via Public Key Hash XOR (BSidesSF 2026)
|
|
190
|
+
|
|
191
|
+
**Pattern (ran-somewhere):** Hybrid RSA+AES encryption where the AES key is derived as `SHA256(DER_encoded_public_key) XOR seed`, with the seed hardcoded or predictable. Since the public key is public, the AES key is fully recoverable without the RSA private key.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from Crypto.PublicKey import RSA
|
|
195
|
+
from Crypto.Cipher import AES
|
|
196
|
+
from hashlib import sha256
|
|
197
|
+
|
|
198
|
+
# Public key is available
|
|
199
|
+
pubkey = RSA.import_key(open("public.pem").read())
|
|
200
|
+
der_bytes = pubkey.export_key("DER")
|
|
201
|
+
|
|
202
|
+
# Seed from challenge (hardcoded/predictable)
|
|
203
|
+
seed = b'BSidesSFCTF2026!'
|
|
204
|
+
|
|
205
|
+
# Derive AES key the same way the encryptor did
|
|
206
|
+
key_hash = sha256(der_bytes).digest()
|
|
207
|
+
aes_key = bytes(a ^ b for a, b in zip(key_hash, seed.ljust(32, b'\x00')))
|
|
208
|
+
|
|
209
|
+
# Decrypt
|
|
210
|
+
ct = open("flag.enc", "rb").read()
|
|
211
|
+
iv, ct_body = ct[:16], ct[16:]
|
|
212
|
+
cipher = AES.new(aes_key, AES.MODE_CBC, iv)
|
|
213
|
+
plaintext = cipher.decrypt(ct_body)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Key insight:** Key derivation that incorporates only public information (public keys, known constants) provides zero security regardless of the hash function used. The "hybrid" design creates a false sense of security — RSA protects nothing if the AES key doesn't depend on the RSA private key.
|
|
217
|
+
|
|
218
|
+
**When to recognize:** Challenge provides both a public key AND an encrypted file, but no private key or ciphertext for RSA. Look for key derivation code that hashes the public key, uses the public key's modulus/exponent as seed material, or XORs with a constant.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## HMAC-CRC Linearity Attack (Boston Key Party 2016)
|
|
223
|
+
|
|
224
|
+
**Pattern:** HMAC constructed with CRC as the hash function is completely broken because CRC is linear over GF(2). The key is directly recoverable from a single message-MAC pair via polynomial arithmetic over GF(2^64).
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
# CRC is linear: CRC(a XOR b) = CRC(a) XOR CRC(b)
|
|
228
|
+
# HMAC-CRC(key, msg) = CRC(key_opad || CRC(key_ipad || msg))
|
|
229
|
+
# Rewrite as polynomial in GF(2): K = known_terms * inverse(x^(128+M) + x^128) mod CRC_POLY
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**Key insight:** CRC's linearity over GF(2) means HMAC-CRC provides zero security. Always verify the underlying hash function is non-linear before trusting HMAC.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## DES Weak Keys in OFB Mode (Boston Key Party 2016)
|
|
237
|
+
|
|
238
|
+
**Pattern:** DES has 4 weak keys where `E(E(P,K),K) = P` (encryption is self-inverse). In OFB (Output Feedback) mode this causes the keystream to cycle with period 2: even blocks XOR with IV, odd blocks with E(IV,K). Reduces to a 16-byte repeating XOR key.
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
# DES weak keys: 0x0000000000000000, 0xFFFFFFFFFFFFFFFF,
|
|
242
|
+
# 0xE1E1E1E1F0F0F0F0, 0x1E1E1E1E0F0F0F0F
|
|
243
|
+
# OFB with weak key: keystream = [IV, E(IV,K), IV, E(IV,K), ...]
|
|
244
|
+
# Recovery: try all 4 weak keys; or treat as 16-byte repeating XOR
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Key insight:** DES weak keys cause OFB keystream to cycle with period 2. When you see DES+OFB, always try the 4 weak keys first.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Square Attack on Reduced-Round AES (0CTF 2016)
|
|
252
|
+
|
|
253
|
+
**Pattern:** 4-round AES is vulnerable to the square (integral) attack. Choose 256 plaintexts differing in one byte (a "lambda set"). After 3 rounds, the XOR sum at any byte position equals 0. Guess one byte of the last round key and partially decrypt -- if XOR sum is 0, the guess is correct.
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
# For each byte position in the last round key:
|
|
257
|
+
for candidate in range(256):
|
|
258
|
+
xor_sum = 0
|
|
259
|
+
for ct in ciphertexts:
|
|
260
|
+
xor_sum ^= inv_sub_bytes(ct[pos] ^ candidate)
|
|
261
|
+
if xor_sum == 0:
|
|
262
|
+
key_byte = candidate # correct guess
|
|
263
|
+
# Reduces 2^128 key recovery to ~16 * 256 = 4096 operations
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Key insight:** Integral cryptanalysis exploits the "balanced" property (XOR-sum = 0) that propagates through AES rounds. Effective against 4-round AES; 5+ rounds require more sophisticated variants.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## SRP (Secure Remote Password) Protocol Bypass via Modular Arithmetic (ASIS CTF Finals 2016)
|
|
271
|
+
|
|
272
|
+
SRP implementations that only check `A != 0` and `A != N` can be bypassed by sending `A = 2*N`, causing the server to compute a zero session key.
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
from hashlib import sha256
|
|
276
|
+
import hmac
|
|
277
|
+
|
|
278
|
+
# SRP protocol: server computes session key from A (client's public value)
|
|
279
|
+
# S = (A * v^u) ^ b mod N
|
|
280
|
+
# If A = 2*N: S = (2*N * v^u) ^ b mod N = 0 (since 2*N mod N = 0)
|
|
281
|
+
|
|
282
|
+
N = server_modulus
|
|
283
|
+
# Send A = 2*N (bypasses checks for A != 0 and A != N)
|
|
284
|
+
A_malicious = 2 * N
|
|
285
|
+
|
|
286
|
+
# Server computes S = 0, so session key K = SHA256(0)
|
|
287
|
+
K = sha256(b'\x00').digest()
|
|
288
|
+
|
|
289
|
+
# Now compute valid HMAC proof with known K
|
|
290
|
+
proof = hmac.new(K, salt, sha256).hexdigest()
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**Key insight:** SRP implementations must validate `A % N != 0`, not just `A != 0` and `A != N`. Sending `A = k*N` for any integer k forces the shared secret to zero, allowing authentication without knowing the password.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Modified AES S-Box Brute-Force Recovery (H4ckIT CTF 2016)
|
|
298
|
+
|
|
299
|
+
AES implementation with a custom S-Box created by swapping 3 elements of the standard S-Box. Brute-force all C(256,3) * 2 = 5,527,040 possible permutations.
|
|
300
|
+
|
|
301
|
+
```cpp
|
|
302
|
+
// Three elements swapped from standard AES S-Box
|
|
303
|
+
// Total permutations: C(256,3) * 2 = ~5.5 million (feasible to brute-force)
|
|
304
|
+
#include <openssl/aes.h>
|
|
305
|
+
|
|
306
|
+
void bruteforce_sbox(uint8_t ciphertext[], uint8_t key[], int ct_len) {
|
|
307
|
+
uint8_t standard_sbox[256]; // standard AES S-Box
|
|
308
|
+
// Try all 3-element swaps
|
|
309
|
+
for (int i = 0; i < 256; i++)
|
|
310
|
+
for (int j = i+1; j < 256; j++)
|
|
311
|
+
for (int k = j+1; k < 256; k++) {
|
|
312
|
+
// Swap pairs: (i,j), (i,k), (j,k)
|
|
313
|
+
uint8_t sbox[256];
|
|
314
|
+
memcpy(sbox, standard_sbox, 256);
|
|
315
|
+
swap(sbox[i], sbox[j]); // try each 2-element swap from the triple
|
|
316
|
+
// Decrypt and check for valid plaintext
|
|
317
|
+
if (try_decrypt_with_sbox(sbox, ciphertext, key, ct_len))
|
|
318
|
+
return; // found it
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Key insight:** When a custom AES S-Box differs from standard by only a few element swaps, the search space is small enough to brute-force. For 3 swapped elements: C(256,3) permutation groups times the swap combinations within each group.
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## AES-ECB Byte-at-a-Time Chosen Plaintext (ABCTF 2016)
|
|
328
|
+
|
|
329
|
+
**Pattern (Encryption Service):** Server encrypts `user_input || secret_suffix` under AES-ECB. Recover the secret suffix one byte at a time by controlling the input length.
|
|
330
|
+
|
|
331
|
+
1. Send inputs of decreasing length to push one unknown byte into a known block position
|
|
332
|
+
2. For each position, try all 256 byte values and compare the encrypted block:
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
from pwn import *
|
|
336
|
+
import cryptanalib as ca # FeatherDuster's cryptanalib
|
|
337
|
+
|
|
338
|
+
def oracle(pt):
|
|
339
|
+
"""Send plaintext, receive ECB-encrypted ciphertext."""
|
|
340
|
+
r = remote('target', 7765)
|
|
341
|
+
r.recvuntil('Send me some hex-encoded data to encrypt:\n')
|
|
342
|
+
r.sendline(pt.hex())
|
|
343
|
+
r.recvuntil('Here you go:')
|
|
344
|
+
ct = bytes.fromhex(r.recvline().strip().decode())
|
|
345
|
+
r.close()
|
|
346
|
+
return ct
|
|
347
|
+
|
|
348
|
+
# Automated byte-at-a-time recovery
|
|
349
|
+
flag = ca.ecb_cpa_decrypt(oracle, block_size=16, verbose=True)
|
|
350
|
+
print(flag)
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Manual approach without library:**
|
|
354
|
+
```python
|
|
355
|
+
block_size = 16
|
|
356
|
+
known = b''
|
|
357
|
+
|
|
358
|
+
for i in range(len(secret)):
|
|
359
|
+
# Pad so next unknown byte is at end of a block
|
|
360
|
+
pad_len = block_size - 1 - (len(known) % block_size)
|
|
361
|
+
pad = b'A' * pad_len
|
|
362
|
+
|
|
363
|
+
# Get target block
|
|
364
|
+
target_ct = oracle(pad)
|
|
365
|
+
target_block_idx = (pad_len + len(known)) // block_size
|
|
366
|
+
target_block = target_ct[target_block_idx*16:(target_block_idx+1)*16]
|
|
367
|
+
|
|
368
|
+
# Try all 256 byte values
|
|
369
|
+
for byte_val in range(256):
|
|
370
|
+
test = pad + known + bytes([byte_val])
|
|
371
|
+
test_ct = oracle(test)
|
|
372
|
+
if test_ct[target_block_idx*16:(target_block_idx+1)*16] == target_block:
|
|
373
|
+
known += bytes([byte_val])
|
|
374
|
+
break
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**Key insight:** ECB mode encrypts identical plaintext blocks to identical ciphertext blocks. By controlling the prefix length, the attacker shifts one unknown byte at a time to a position where it completes a known block prefix. Comparing the target ciphertext block against all 256 possibilities recovers each byte in at most 256 queries. Total queries: ~256 * secret_length. Tool: FeatherDuster's `cryptanalib.ecb_cpa_decrypt()` automates this completely.
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## AES-ECB Cut-and-Paste Block Manipulation (NDH Quals 2016)
|
|
382
|
+
|
|
383
|
+
**Pattern (Toil33t):** Server encrypts JSON session data in AES-ECB mode. Fields like `is_admin: false` span predictable block boundaries. Construct chosen plaintext blocks via registration, then splice ciphertext blocks to change `false` to `true`.
|
|
384
|
+
|
|
385
|
+
1. Detect ECB mode: register with repeating username (e.g., 'A' * 64), look for identical ciphertext blocks
|
|
386
|
+
2. Map block boundaries by varying username length until block count changes
|
|
387
|
+
3. Determine field ordering by independently varying username and email lengths
|
|
388
|
+
4. Craft target block containing `true` by aligning it at a block boundary via padding:
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
# Align "true" at start of a block using space padding (JSON ignores whitespace)
|
|
392
|
+
# Original: {"username": "AA", "is_admin": false, "email": ""}
|
|
393
|
+
# Target: {"username": "AA", "is_admin": true, "email": ""}
|
|
394
|
+
# ^-- 16-byte block boundary
|
|
395
|
+
|
|
396
|
+
# Get the " true" block from:
|
|
397
|
+
username = "AAA" + " " * 12 + "true"
|
|
398
|
+
# Extract block 2 of the resulting ciphertext
|
|
399
|
+
|
|
400
|
+
# Get prefix blocks from a short username
|
|
401
|
+
# Get suffix block from a padded username
|
|
402
|
+
# Concatenate: prefix_blocks + true_block + suffix_block
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**Key insight:** AES-ECB encrypts each 16-byte block independently with no chaining. Identical plaintext blocks produce identical ciphertext blocks, allowing block-level cut-and-paste. JSON's tolerance for extra whitespace enables block alignment without breaking parsing. The attack requires: (a) detecting ECB via repeated blocks, (b) mapping field layout via length probing, (c) crafting and splicing blocks.
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## AES-CBC IV Bit-Flip Authentication Bypass (Google CTF 2016)
|
|
410
|
+
|
|
411
|
+
**Pattern (Eucalypt Forest):** Server encrypts JSON session blob under AES-CBC and returns both IV and ciphertext as a cookie. No integrity check (no MAC/HMAC). Flip bits in the IV to change the first plaintext block.
|
|
412
|
+
|
|
413
|
+
1. Register with username one bit away from target (e.g., `` `dmin `` instead of `admin` — flip LSB of 'a')
|
|
414
|
+
2. Identify the IV byte position corresponding to the target character in the first block
|
|
415
|
+
3. Flip the same bit in the IV byte — XOR propagates directly to the plaintext:
|
|
416
|
+
|
|
417
|
+
```python
|
|
418
|
+
import binascii
|
|
419
|
+
cookie = binascii.unhexlify(auth_cookie)
|
|
420
|
+
iv = bytearray(cookie[:16])
|
|
421
|
+
ciphertext = cookie[16:]
|
|
422
|
+
|
|
423
|
+
# Flip LSB of byte at position where 'a'/'`' appears in first block
|
|
424
|
+
# Position depends on JSON structure: {"username":"`dmin"}
|
|
425
|
+
# 'a' (0x61) vs '`' (0x60) differ only in bit 0
|
|
426
|
+
target_pos = 13 # position of first char of username in block
|
|
427
|
+
iv[target_pos] ^= 0x01
|
|
428
|
+
|
|
429
|
+
forged = binascii.hexlify(bytes(iv) + ciphertext)
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**Key insight:** AES-CBC decryption XORs the previous ciphertext block (or IV for block 0) with the AES-decrypted block. Flipping bit `i` in the IV flips bit `i` in the first plaintext block with no other side effects. This only works when the server performs no integrity verification (no HMAC, AEAD, or authenticated encryption).
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## Rabin Cryptosystem LSB Parity Oracle (PlaidCTF 2016)
|
|
437
|
+
|
|
438
|
+
**Pattern (rabit):** Server encrypts flag with the Rabin cryptosystem (`c = m^2 mod n`) and provides an LSB oracle — for any ciphertext, it returns the least significant bit of the decrypted plaintext. Binary search recovers the full plaintext in `log2(n)` queries.
|
|
439
|
+
|
|
440
|
+
```python
|
|
441
|
+
from Crypto.Util.number import long_to_bytes
|
|
442
|
+
|
|
443
|
+
def lsb_oracle_attack(enc_flag, N, oracle_fn):
|
|
444
|
+
"""Recover plaintext from Rabin/RSA LSB oracle via binary search."""
|
|
445
|
+
lower = 0
|
|
446
|
+
upper = N
|
|
447
|
+
C = enc_flag
|
|
448
|
+
# Rabin: encrypt(2,N) = 4; multiplying ciphertext by 4 doubles plaintext
|
|
449
|
+
e2 = pow(2, 2, N) # For Rabin; use pow(2, e, N) for RSA
|
|
450
|
+
|
|
451
|
+
for i in range(N.bit_length()):
|
|
452
|
+
C = (e2 * C) % N # Multiply plaintext by 2
|
|
453
|
+
lsb = oracle_fn(C)
|
|
454
|
+
if lsb == 1:
|
|
455
|
+
# 2*m > N (odd remainder after mod), increase lower bound
|
|
456
|
+
lower = (upper + lower) // 2
|
|
457
|
+
else:
|
|
458
|
+
# 2*m < N (even remainder), decrease upper bound
|
|
459
|
+
upper = (upper + lower) // 2
|
|
460
|
+
# Progressive decryption visible:
|
|
461
|
+
print(long_to_bytes(upper))
|
|
462
|
+
return upper
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
**Key insight:** Rabin (and textbook RSA) are multiplicatively homomorphic: multiplying ciphertext by `2^e mod N` doubles the plaintext mod N. Since N is odd, doubling causes a modular wraparound iff the plaintext exceeds `N/2`, which changes the LSB parity. This creates a binary search: each oracle query halves the candidate range, recovering the full plaintext in exactly `log2(N)` queries (~1024 for RSA-1024).
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## PBKDF2 Pre-Hash Bypass for Long Passwords (BackdoorCTF 2016)
|
|
470
|
+
|
|
471
|
+
**Pattern (Mindblown):** PBKDF2 (and HMAC generally) pre-hashes passwords longer than the hash block size (64 bytes for SHA-1/SHA-256). If the target password exceeds 64 bytes, `PBKDF2(password)` equals `PBKDF2(SHA1(password))`, enabling authentication with the hash instead of the original password.
|
|
472
|
+
|
|
473
|
+
```python
|
|
474
|
+
import hashlib
|
|
475
|
+
|
|
476
|
+
original_password = "complexPasswordWhichContainsManyCharactersWithRandomSuffixeghjrjg"
|
|
477
|
+
# len > 64, so HMAC pre-hashes it
|
|
478
|
+
equivalent = hashlib.sha1(original_password.encode()).digest()
|
|
479
|
+
# Login with equivalent — PBKDF2 produces the same derived key
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
**Key insight:** HMAC's inner construction is `H((K XOR ipad) || message)`. When the key (password) exceeds the hash block size, HMAC first reduces it via `K = H(password)`. This means `HMAC(long_password, ...)` equals `HMAC(H(long_password), ...)`. Any system using PBKDF2/HMAC with a `!==` identity check after hash comparison is vulnerable when passwords exceed 64 bytes. This is a HMAC specification behavior, not an implementation bug.
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## MD5 Multi-Collision via Fastcol (BackdoorCTF 2016)
|
|
487
|
+
|
|
488
|
+
**Pattern (Forge):** Generate 2^k files with identical MD5 hashes by chaining `fastcol` (Marc Stevens' tool). Each run produces two suffixes (A, B) that when appended yield the same MD5. Chain 3 runs to produce 8 collisions:
|
|
489
|
+
|
|
490
|
+
```text
|
|
491
|
+
[prefix][suffix1A][suffix2A][suffix3A] \
|
|
492
|
+
[prefix][suffix1A][suffix2A][suffix3B] |
|
|
493
|
+
[prefix][suffix1A][suffix2B][suffix3A] |-- all have same MD5
|
|
494
|
+
[prefix][suffix1A][suffix2B][suffix3B] |
|
|
495
|
+
[prefix][suffix1B][suffix2A][suffix3A] |
|
|
496
|
+
[prefix][suffix1B][suffix2B][suffix3B] /
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
# Install: git clone https://github.com/cr-marcstevens/hashclash
|
|
501
|
+
# Generate one collision pair (~minutes on modern CPU):
|
|
502
|
+
./fastcol -o suffix1A.bin suffix1B.bin < prefix.bin
|
|
503
|
+
# Chain: append suffix1A to prefix, run fastcol again for suffix2A/2B, etc.
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
**Key insight:** MD5 collision generation is practical with `fastcol` (~minutes per pair). Because MD5 uses Merkle-Damgard construction, collisions compose: if `H(A||X) == H(A||Y)`, then `H(A||X||Z) == H(A||Y||Z)` for any suffix Z. Chaining k collision pairs produces 2^k files with identical MD5. For CRC32 collisions, append bytes after PNG IEND chunk (parsers ignore trailing data) and brute-force the 4-byte CRC adjustment.
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
## GHASH Key Recovery over Prime Modulus (nullcon HackIM 2019)
|
|
511
|
+
|
|
512
|
+
**Pattern (GenuineCounterMode):** A custom GCM-like scheme computes `tag = c + sum(b_i * H^(i+1)) mod n` where `n` is a 128-bit prime (not `GF(2^128)`). The 12-byte nonce has 10 bytes fixed from the session ID plus 2 random bytes, so nonce collisions arrive in roughly 256 encryption queries (birthday bound). With two colliding nonces, the equations for `tag1` and `tag2` share the same `c = E_K(nonce || counter)`, so subtracting eliminates `c` and leaves a linear equation in `H` modulo prime `n` — solvable by a single modular inverse, not `GF(2^128)` polynomial factoring.
|
|
513
|
+
|
|
514
|
+
```python
|
|
515
|
+
from Crypto.Util.number import bytes_to_long, long_to_bytes, inverse
|
|
516
|
+
|
|
517
|
+
n = 327989969870981036659934487747327553919 # prime modulus (not GF(2^128))
|
|
518
|
+
|
|
519
|
+
# 1. Request encryptions with single-block messages until two share a nonce
|
|
520
|
+
# 2. With colliding (nonce, ct1, tag1) and (nonce, ct2, tag2):
|
|
521
|
+
m1 = bytes_to_long(ct1) # single 16-byte block
|
|
522
|
+
m2 = bytes_to_long(ct2)
|
|
523
|
+
t1 = bytes_to_long(tag1)
|
|
524
|
+
t2 = bytes_to_long(tag2)
|
|
525
|
+
H = ((t1 - t2) * inverse(m1 - m2, n)) % n
|
|
526
|
+
|
|
527
|
+
# 3. Forge: encrypt "may i please have the galf", flip CTR bytes to 'flag'
|
|
528
|
+
# then recompute tag using recovered H and c = tag - sum(b_i * H^(i+1))
|
|
529
|
+
c0 = (t1 - sum(bytes_to_long(b) * pow(H, i + 1, n) for i, b in enumerate(blocks1))) % n
|
|
530
|
+
forged_tag = (c0 + sum(bytes_to_long(b) * pow(H, i + 1, n) for i, b in enumerate(forged_blocks))) % n
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
**Key insight:** GCM's security rests on `GHASH` operating over `GF(2^128)` where inversion is hard without the key. Swapping the modulus to a plain prime `n` collapses the authentication to textbook linear algebra mod `n` — two nonce-colliding tags give one linear equation per unknown, solved with `inverse(m1 - m2, n)`. Short-nonce (2 random bytes) designs guarantee birthday collisions in ~256 queries. Contrast with the `GF(2^128)` AES-GCM forbidden attack in [modern-ciphers.md](modern-ciphers.md#aes-gcm-nonce-reuse--forbidden-attack), which needs polynomial factoring over binary fields.
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
## SHA-1 Length Extension Plus AES-CBC Cookie Forgery (BSidesSF 2019)
|
|
538
|
+
|
|
539
|
+
**Pattern (decrypto):** Cookie stores `user = iv || AES-CBC(key, plaintext)` plus a separate `signature = SHA1(secret || decrypt(ct))` tag. The session cookie leaks the AES key (e.g. trailing 32 bytes of a base64 session blob). To forge `UID 0`, length-extend the signature with `\nUID 0\n`, decrypt the current ciphertext to learn the plaintext, append hashpump's padding + extension, re-encrypt with the known key, and send both updated `user` and `signature`.
|
|
540
|
+
|
|
541
|
+
```python
|
|
542
|
+
import hashpumpy, binascii, base64, urllib
|
|
543
|
+
from Crypto.Cipher import AES
|
|
544
|
+
|
|
545
|
+
# Extract AES key from leaked rack.session cookie
|
|
546
|
+
key = base64.b64decode(urllib.unquote(cookies['rack.session'].split('--')[0]))[-32:]
|
|
547
|
+
user = binascii.unhexlify(cookies['user'])
|
|
548
|
+
iv, ct = user[:16], user[16:]
|
|
549
|
+
|
|
550
|
+
def decrypt(c): return AES.new(key, AES.MODE_CBC, iv).decrypt(c).rstrip(b'\x10\x0f\x0e...')
|
|
551
|
+
def encrypt(p): pad = 16 - len(p) % 16; return AES.new(key, AES.MODE_CBC, iv).encrypt(p + bytes([pad])*pad)
|
|
552
|
+
|
|
553
|
+
# Length-extend signature (secret length guessed = 8)
|
|
554
|
+
new_sig, new_plain = hashpumpy.hashpump(cookies['signature'], decrypt(ct), b'\nUID 0\n', 8)
|
|
555
|
+
cookies['signature'] = new_sig
|
|
556
|
+
cookies['user'] = binascii.hexlify(iv + encrypt(new_plain))
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
**Key insight:** Hash length extension applies whenever the MAC is `H(secret || data)` over a Merkle-Damgard hash (MD5/SHA-1/SHA-256). If the *same* `data` also lives inside a *separately* keyed cipher whose key is recoverable, you can combine primitives: hashpumpy produces the extended plaintext and new tag, then you re-encrypt with the leaked AES key so the server's CBC decryption matches the extended string the signature now covers. Parse-order quirks (later fields overriding earlier ones) let the appended `\nUID 0\n` win.
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
See [modern-ciphers-3.md](modern-ciphers-3.md) for custom hash reversal, CRC32 brute-force, noisy RSA LSB oracle, sponge collisions, CBC IV forgery + block truncation, padding oracle + bit-flip command injection, SPN S-box intersection, AES-CFB IV recovery, three-round XOR, Unicode decode side channel, SHA-256 basis attack, MAC forgery via XOR block cancellation, and bit-by-bit HMAC key recovery.
|