BreizhCTF 2026 - Totally Secure

Totally Secure
- Difficulty: Easy
- Category: Forensic
- Author: Zlippy
Description
A friend set up his own “secure” server using copy-paste and ChatGPT prompts. He’s very proud and keeps saying everything is “encrypted” and nobody can see anything on the network.
He shared a network capture and some files recovered from his machine, convinced you won’t understand a thing. Your mission: prove him wrong and recover what was traveling through his supposedly unbreakable communications.
Files:
traffic.pcapng— network capture of the client browsing to the servertriage_server.tar.gz— forensic dump of the server (configs, processes, logs, filesystem…)
Solve
Step 1 — Map the network traffic
List all TCP conversations from the capture:
tshark -r traffic.pcapng -q -z conv,tcpOne IP stands out: 10.90.35.19, on the same subnet as the client (10.90.35.143). That’s the “secure” server. Filtering on it:
tshark -r traffic.pcapng | grep "10.90.35.19" 2987 16.625631355 10.90.35.143 → 10.90.35.19 TCP 74 39470 → 443 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 SACK_PERM TSval=697595375 TSecr=0 WS=128
2988 16.626162727 10.90.35.19 → 10.90.35.143 TCP 74 443 → 39470 [SYN, ACK] Seq=0 Ack=1 Win=65160 Len=0 MSS=1460 SACK_PERM TSval=759725241 TSecr=697595375 WS=128
2989 16.626210132 10.90.35.143 → 10.90.35.19 TCP 66 39470 → 443 [ACK] Seq=1 Ack=1 Win=64256 Len=0 TSval=697595376 TSecr=759725241
2990 16.630862909 10.90.35.143 → 10.90.35.19 TLSv1 226 Client Hello (SNI=pleasedontdothat.local)
2991 16.631297136 10.90.35.19 → 10.90.35.143 TCP 66 443 → 39470 [ACK] Seq=1 Ack=161 Win=65024 Len=0 TSval=759725246 TSecr=697595381
2992 16.632325670 10.90.35.19 → 10.90.35.143 TLSv1.2 1092 Server Hello, Certificate, Server Hello Done
2993 16.632349897 10.90.35.143 → 10.90.35.19 TCP 66 39470 → 443 [ACK] Seq=161 Ack=1027 Win=67200 Len=0 TSval=697595382 TSecr=759725247
2994 16.633977211 10.90.35.143 → 10.90.35.19 TLSv1.2 412 Client Key Exchange, Change Cipher Spec, Encrypted Handshake Message
2995 16.637547905 10.90.35.19 → 10.90.35.143 TLSv1.2 145 Change Cipher Spec, Encrypted Handshake Message
2996 16.637802245 10.90.35.143 → 10.90.35.19 TLSv1.2 203 Application Data
2997 16.638399440 10.90.35.19 → 10.90.35.143 TLSv1.2 3883 Application Data
2998 16.638489182 10.90.35.143 → 10.90.35.19 TCP 66 39470 → 443 [ACK] Seq=644 Ack=4923 Win=63488 Len=0 TSval=697595388 TSecr=759725253
2999 16.638914193 10.90.35.143 → 10.90.35.19 TLSv1.2 123 Encrypted Alert
3000 16.639032578 10.90.35.143 → 10.90.35.19 TCP 66 39470 → 443 [FIN, ACK] Seq=701 Ack=4923 Win=63488 Len=0 TSval=697595389 TSecr=759725253
3001 16.639297375 10.90.35.19 → 10.90.35.143 TCP 66 443 → 39470 [FIN, ACK] Seq=4923 Ack=702 Win=64640 Len=0 TSval=759725254 TSecr=697595389
3002 16.639322289 10.90.35.143 → 10.90.35.19 TCP 66 39470 → 443 [ACK] Seq=702 Ack=4924 Win=63488 Len=0 TSval=697595389 TSecr=759725254The Client Hello reveals the SNI pleasedontdothat.local.
What is SNI? The SNI (Server Name Indication) is a TLS extension that lets the client announce the target domain before the session is encrypted. It is always visible in plaintext in a network capture, even in TLSv1.3. Here,
pleasedontdothat.localis readable without decrypting anything.
Step 2 — Identify the negotiated cipher suite
Inspect the Server Hello:
tshark -r traffic.pcapng \
-Y "ip.addr == 10.90.35.19 && tls.handshake.type == 2" \
-V | grep "Cipher Suite"Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f)This is where everything falls apart. TLS_RSA_WITH_AES_128_CBC_SHA uses a static RSA key exchange:
- The client generates a pre-master secret
- It encrypts it with the server’s public key and sends it
- The server decrypts it with its RSA private key
- Both sides derive session keys from it
Direct consequence: anyone who has the server’s private RSA key can decrypt any past captured session. This is the opposite of Perfect Forward Secrecy (PFS), provided by ECDHE/DHE where session keys are ephemeral and independent of the private key.
Confirmed in the server’s nginx config from the triage:
triage_server/[root]/etc/nginx/sites-available/chall:
ssl_protocols TLSv1.2;
ssl_ciphers "AES128-SHA:AES256-SHA:AES128-SHA256:AES256-SHA256:DES-CBC3-SHA";
ssl_prefer_server_ciphers on;
ssl_session_cache off;
ssl_session_tickets on;Every cipher listed is an RSA variant with no PFS.
Step 3 — Retrieve the private key from the triage
The triage contains a copy of the server’s filesystem. The nginx SSL files are right there:
ls triage_server/[root]/etc/nginx/ssl/
# server.crt server.keyThe RSA private key is sitting in plain sight in server.key.
Step 4 — Decrypt the TLS traffic
tshark natively supports TLS RSA decryption via tls.keys_list. It uses the private key to decrypt the pre-master secret from the handshake, then reconstructs the session keys to decrypt the application data.
cp "triage_server/[root]/etc/nginx/ssl/server.key" .
tshark -r traffic.pcapng \
-o "tls.keys_list:10.90.35.19,443,http,server.key" \
-Y "ip.addr == 10.90.35.19 && http.response" \
-T fields -e http.file_data \
| python3 -c "import sys; print(bytes.fromhex(sys.stdin.read().strip()).decode())"-o "tls.keys_list:IP,PORT,PROTO,FILE"— provides the RSA key to tshark for TLS decryption-Y "... && http.response"— filters HTTP packets after decryption (without the key, tshark only sees opaqueApplication Data)-T fields -e http.file_data— extracts only the HTTP response body- The Python pipe converts hex output from tshark into readable text
The decrypted HTTP response is an HTML page containing:
<div class="seccccret-value">BZHCTF{Pl34se_D0nT_D0_Th4t_1n_Pr0d!!!!}</div>Takeaway: The problem is not TLS itself, but the misconfiguration. Using RSA cipher suites without PFS means the server’s private key is a master key protecting all past and future traffic. A single leak (server compromise, forensic triage) retroactively compromises every captured session. The fix: use TLS 1.3 or enforce ECDHE/DHE-only cipher suites.
Flag : BZHCTF{Pl34se_D0nT_D0_Th4t_1n_Pr0d!!!!}