Event: CDDC 2026
Category: Network / Crypto
Flag: cddc2026{1_4m_n0t_r3ady_t0_f4ce_th3_truth_y3t}
Story
Headquarters has identified one of Defender's team members as a traitor. Following HQ's order ("Execute a full deletion of the suspect and the comms data, effective immediately. — Head__art__s"), Defender stopped the teammate. Growing doubts led Defender to secretly examine the last trace the teammate had left behind: a hastily sent transmission whose security may have been somewhat fragile.
We're given A FRAGILE TRUTH.pcapng containing 21 TLS sessions to port 4433 on 172.18.0.2.
Recon
Total packets : 399
TLS port : 4433 (21 connections, 21 client/server flow pairs)
Clients : 172.18.0.10 (×8), .11 (×8), .13 (×3), .12 (×1), .20 (×1)
Reassembling the streams and scanning every Server Hello reveals two cipher suites in use:
| cipher | suite | seen on |
|---|---|---|
0xC030 |
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 |
20 sessions |
0x0035 |
TLS_RSA_WITH_AES_256_CBC_SHA |
1 session (172.18.0.20:43311) |
That one outlier is the suspect. ECDHE provides forward secrecy, but TLS_RSA does not — the client encrypts the pre-master secret directly with the server's RSA public key. Get the private key, decrypt the session.
The certificate
Extracting the server cert from the Certificate handshake message:
Subject : CN=vulnerable.ctf, O=InternalPortal Corp, C=KR
Public-Key : RSA, 511 bits (!!)
Exponent : 65537
Modulus N : 5F61803FB6E70DB04C052A2212...EBA551E6D
A 511-bit modulus screams "factor me," but RSA-512 (≈154 digits) is still hours of CPU on commodity hardware with NFS — not a quick lookup. FactorDB has it as C154 with no factorization stored.
What does "fragile" mean here?
Brute factoring is the obvious-but-slow path. The challenge title hints at something more specific: a fragile choice of prime.
Pollard's p − 1 algorithm finds a prime factor p of N whenever p − 1 is B-smooth — i.e., all of its prime factors are ≤ B. A properly generated RSA prime should have at least one large factor in p − 1 to defeat this; the suspect, "hastily sending a transmission to evade HQ's surveillance," skipped that check.
I let yafu's auto-factor pipeline run:
ecm: 214/214 curves on C154, B1=50k ... no factor (small ECM bucket)
pm1: starting B1 = 3750K, B2 = gmp-ecm default on C154
pm1: Process took 0.6547 seconds.
pm1: found prp77 factor = 72596913078035655369042280105449632221957231318118875016708473779461685758183
Total wall time including ECM warmup: 3.4 seconds. P − 1 is smooth at B1 = 3.75M.
p = 68811481578421750518769574540287122730773374684179588971811640397724817198987
q = 72596913078035655369042280105449632221957231318118875016708473779461685758183
Sanity check: p · q = N ✓.
Recovering the session keys
With (p, q) we have d = e⁻¹ mod φ(N).
Walking the 172.18.0.20:43311 flow:
- Server Hello extensions advertise
extended_master_secret(0x0017) andencrypt_then_mac(0x0016) — both matter for derivation and record framing. - ClientKeyExchange carries the PKCS#1 v1.5–encrypted PMS:
enc_pms = 01854f57ba547676...832db23a (64 bytes) - RSA-decrypt and strip PKCS#1 v1.5 padding:
pms = 03 03 21 69 51 3f 99 cf ... (48 bytes; leading 03 03 = TLS 1.2)
Because the handshake negotiated Extended Master Secret (RFC 7627), the master is derived from a transcript hash, not the raw randoms:
session_hash = SHA-256( ClientHello || ServerHello || Certificate || ServerHelloDone || ClientKeyExchange )
master_secret = PRF-SHA256(pms, "extended master secret", session_hash, 48)
Then the standard key block:
key_block = PRF-SHA256(master, "key expansion",
server_random || client_random,
20+20+32+32 bytes)
↑ ↑ ↑ ↑
CMAC SMAC CKEY SKEY
(SHA-1 MAC = 20 bytes, AES-256 key = 32 bytes; AES-CBC IV is explicit per record in TLS 1.2.)
Decrypting the records
With encrypt-then-MAC, each encrypted record is laid out as:
[ explicit IV (16) ] [ ciphertext (16·n) ] [ HMAC-SHA1 (20) ]
— and the MAC covers the encrypted body, not the plaintext. The earlier "ct length 52 not multiple of 16" failure was the giveaway: under standard MAC-then-encrypt the math doesn't work, under EtM it does (52 - 20 = 32 = 16·2).
Decrypt → strip CBC padding → server's seq=1 application-data record yields:
HTTP/1.0 200 OK
Content-Type: text/plain
TO. my dear
Hi, my dear.
What's going on at Head these days?
Lately, it feels like we're getting unreasonable orders all the time.
My colleagues are disappearing one by one, and I'm really scared.
I'm worried something might have happened to you.
If anything is wrong, please be sure to reply.
Since I'm sending this while evading HQ's surveillance,
I'm using a somewhat old-fashioned method.
I trust you'll be able to decode this message.
I look forward to seeing you again.
cddc2026{1_4m_n0t_r3ady_t0_f4ce_th3_truth_y3t}
FROM. Your dear
The client request, for completeness:
GET /flag.txt HTTP/1.0
Host: portal.internal
User-Agent: VerificationAgent/1.0
All MACs verify. ✓
Lessons / observations
- Cipher selection matters more than key size narrative. Twenty sessions used ECDHE and were forward-secret; one downgrade-style session used static RSA and exposed everything.
- "512-bit RSA is broken" usually means days of NFS — but not if
p − 1is smooth. Always pair a primality test with a Pollard p − 1 / p + 1 sanity check on your generated primes (most libraries do, ad-hoc CTF generators don't). - Modern TLS 1.2 stacks negotiate EMS + EtM by default. Any TLS-decrypt script that assumes the textbook RFC 5246 layout will silently fail; check the Server Hello extensions before deriving keys.
Tooling used
scapy— pcap reassembly and TLS record parsingopenssl x509— pull the modulus out of the certyafu(built from source againstgmp-ecm) — automated factor pipeline that ran ECM then P − 1pycryptodome— AES-256-CBC for record decryption- Pure-Python PRF-SHA256, HMAC-SHA1 for TLS key derivation
Final flag
cddc2026{1_4m_n0t_r3ady_t0_f4ce_th3_truth_y3t}