Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
ForensicsMedium

Quick Mistake

CTF writeup for Quick Mistake from niteCTF

//Quick Mistake

>TL;DR

The attacker abused an admin telemetry endpoint to make the server leak a TLS/QUIC key log (via encrypted telemetry datagrams). With that key log, we can decrypt the HTTP/3 (QUIC) traffic in the PCAP, download an internal /source bundle that contains an .env file with AES_FLAG_KEY, and then decrypt the /flag response to get flag part 2.

Final flag:

nite{192.0.2.66_2457ce19cb87e0eb_qu1c_d4t4gr4m_pwn3d}


>Files provided

Inside the handout:

  • Chal.pcap — incident packet capture

Tools used:

  • tshark (Wireshark CLI)
  • Python 3 + cryptography

>1) Quick PCAP recon (what protocols are present?)

First, confirm what we’re dealing with:

bash
tshark -r handout/Chal.pcap -q -z io,phs

Key takeaways from the protocol hierarchy:

  • There is HTTP over TCP (a few frames)
  • There is QUIC traffic (UDP) — this strongly suggests HTTP/3

Then list the heaviest endpoints:

bash
tshark -r handout/Chal.pcap -q -z endpoints,ip

You’ll see a small set of internal-ish IPs plus one external server:

  • 203.0.113.100 — QUIC server (UDP/4433)
  • 192.0.2.66 — suspicious client that later pulls /source and /flag

To confirm the “interesting” HTTP requests:

bash
tshark -r handout/Chal.pcap -Y http.request \\
  -T fields -E header=y -E separator=$'\\t' \\
  -e frame.time_relative -e ip.src -e ip.dst -e http.host -e http.request.method -e http.request.uri

This shows a standout request:

  • 192.0.2.66 -> 203.0.113.100 GET /telemetry (classic “admin telemetry” smell)

>2) Identify the telemetry leak mechanism

The PCAP contains plaintext JSON sent over UDP that literally references sslkeylog and includes encrypted chunks.

Quick way to spot it:

bash
strings -a handout/Chal.pcap | grep -E 'handshake_init|telemetry_sslkeylog|sslkeylog'

You should find JSON objects like:

  • {"type": "handshake_init", "seed": "…", "salt": "telemetry", "info": "sslkeylog"}
  • {"type": "telemetry_sslkeylog", "seq": 0|1, "nonce_b64": "…", "ct_b64": "…", "tag_b64": "…"}

Locate the exact frames:

bash
tshark -r handout/Chal.pcap -Y 'frame contains "handshake_init"' -T fields -e frame.number
tshark -r handout/Chal.pcap -Y 'frame contains "telemetry_sslkeylog"' -T fields -e frame.number

In this capture, those are:

  • handshake_init: frames 179 / 180 (duplicate)
  • telemetry_sslkeylog: frames 182185 (duplicates)

Extract one copy of each JSON payload as hex:

bash
# handshake_init (pick one)
tshark -r handout/Chal.pcap -Y 'frame.number==179' -T fields -e udp.payload

# chunk 0 (pick one)
tshark -r handout/Chal.pcap -Y 'frame.number==182' -T fields -e udp.payload

# chunk 1 (pick one)
tshark -r handout/Chal.pcap -Y 'frame.number==184' -T fields -e udp.payload

>3) Decrypt the telemetry “sslkeylog” chunks

What’s going on cryptographically?

  • The handshake_init provides a seed plus HKDF parameters:
    • salt = "telemetry"
    • info = "sslkeylog"
  • A key is derived via HKDF-SHA256 (32 bytes)
  • The chunks are encrypted with AES-GCM using base64-encoded nonce, ct, and tag

Decryption script

Run this Python snippet to:

  • Pull the UDP JSON blobs directly from the PCAP
  • Derive the AES-GCM key using HKDF
  • Decrypt chunk 0 + chunk 1
  • Write the output as a standard SSLKEYLOGFILE
bash
python3 - <<'PY'
import base64, json, subprocess
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

PCAP = 'handout/Chal.pcap'

# Frame numbers in this capture (one copy of each)
HANDSHAKE_FRAME = 179
CHUNK0_FRAME = 182
CHUNK1_FRAME = 184

cmd = [
    'tshark','-r',PCAP,
    '-Y', f'frame.number=={HANDSHAKE_FRAME} || frame.number=={CHUNK0_FRAME} || frame.number=={CHUNK1_FRAME}',
    '-T','fields','-E','separator=\\t','-e','frame.number','-e','udp.payload'
]
raw = subprocess.check_output(cmd, text=True).strip().splitlines()
by_frame = {}
for line in raw:
    num, payload_hex = line.split('\\t', 1)
    by_frame[int(num)] = bytes.fromhex(payload_hex.strip())

handshake = json.loads(by_frame[HANDSHAKE_FRAME].decode('utf-8'))
chunk0 = json.loads(by_frame[CHUNK0_FRAME].decode('utf-8'))
chunk1 = json.loads(by_frame[CHUNK1_FRAME].decode('utf-8'))

seed = bytes.fromhex(handshake['seed'])
salt = handshake['salt'].encode('utf-8')
info = handshake['info'].encode('utf-8')

key = HKDF(
    algorithm=hashes.SHA256(),
    length=32,
    salt=salt,
    info=info,
).derive(seed)

aesgcm = AESGCM(key)

def decrypt_chunk(chunk):
    nonce = base64.b64decode(chunk['nonce_b64'])
    ct = base64.b64decode(chunk['ct_b64'])
    tag = base64.b64decode(chunk['tag_b64'])
    return aesgcm.decrypt(nonce, ct + tag, None)

plaintext = decrypt_chunk(chunk0) + decrypt_chunk(chunk1)

out_path = 'handout/sslkeylog.txt'
with open(out_path, 'wb') as f:
    f.write(plaintext)

print('Wrote', out_path)
print(plaintext.decode('utf-8', errors='replace'))
PY

At the end you should have handout/sslkeylog.txt containing entries like:

  • CLIENT_TRAFFIC_SECRET_0 …
  • SERVER_TRAFFIC_SECRET_0 …

That’s exactly what Wireshark/tshark needs to decrypt QUIC.


>4) Decrypt QUIC/HTTP3 and extract the stolen internal data

Now that we have a keylog file, use it to decrypt HTTP/3:

bash
tshark -r handout/Chal.pcap \\
  -o tls.keylog_file:handout/sslkeylog.txt \\
  -Y 'http3.headers.method' \\
  -T fields -E header=y -E separator=$'\\t' \\
  -e frame.number -e ip.src -e ip.dst \\
  -e http3.headers.method -e http3.headers.authority -e http3.headers.path -e http3.headers.status

You should see the attacker (192.0.2.66) request:

  • GET <https://localhost/source>
  • GET <https://localhost/flag>

Identify the attacker CID (Connection ID)

For the /source and /flag transaction, correlate the QUIC connection ID:

bash
tshark -r handout/Chal.pcap \\
  -o tls.keylog_file:handout/sslkeylog.txt \\
  -Y 'frame.number>=360 && frame.number<=390 && quic' \\
  -T fields -E header=y -E separator=$'\\t' \\
  -e frame.number -e ip.src -e ip.dst \\
  -e quic.dcid -e quic.scid \\
  -e http3.headers.path -e http3.headers.method -e http3.headers.status

In this capture, the server-to-attacker traffic uses the destination CID:

  • 2457ce19cb87e0eb

That’s the CID expected in the flag format.


>5) Extract /source (it’s a tar.gz served over HTTP/3)

The /source response body is shown by tshark as a long hex string (it starts with gzip magic 1f8b08).

Extract the decrypted payload from the /source response frame (in this capture: frame 373):

bash
# Save the HTTP/3 data field (hex)
tshark -r handout/Chal.pcap \\
  -o tls.keylog_file:handout/sslkeylog.txt \\
  -Y 'frame.number==373' \\
  -T fields -e http3.data > handout/source_hex.txt

# Convert hex -> gzip bytes
python3 - <<'PY'
from pathlib import Path
hx = Path('handout/source_hex.txt').read_text().strip().replace('\\n','')
Path('handout/source.gz').write_bytes(bytes.fromhex(hx))
print('wrote handout/source.gz')
PY

# Decompress gzip -> tar
python3 - <<'PY'
import gzip
from pathlib import Path
Path('handout/source.tar').write_bytes(gzip.decompress(Path('handout/source.gz').read_bytes()))
print('wrote handout/source.tar')
PY

# Extract tar
mkdir -p handout/source_extract
tar -xf handout/source.tar -C handout/source_extract

# Inspect
ls -la handout/source_extract
cat handout/source_extract/.env

The .env contains the key we need:

  • AES_FLAG_KEY=wEN64tLF1PtOglz3Oorl7su8_GQzmlU2jbFP70cFz7c=

This is a valid Fernet key (URL-safe base64), despite the variable name saying AES.


>6) Extract /flag and decrypt it (flag part 2)

From the decrypted HTTP/3 output, the /flag response body is a Fernet token:

gAAAAABpNXDCHUJ4YqH0Md2p6tzE303L8z5kPpPPWwYYrXUdiyW89eCaWWL1dbYU2JYj7SUvdwySW_egZDRF0fyFGxPua2KoFmd8upKP7cZv55jVp_SzItA=

Decrypt it using the leaked AES_FLAG_KEY:

bash
python3 - <<'PY'
from cryptography.fernet import Fernet

key = b'wEN64tLF1PtOglz3Oorl7su8_GQzmlU2jbFP70cFz7c='
token = 'gAAAAABpNXDCHUJ4YqH0Md2p6tzE303L8z5kPpPPWwYYrXUdiyW89eCaWWL1dbYU2JYj7SUvdwySW_egZDRF0fyFGxPua2KoFmd8upKP7cZv55jVp_SzItA='
print(Fernet(key).decrypt(token.encode()).decode())
PY

Output:

  • qu1c_d4t4gr4m_pwn3d}

That’s flag part 2.


>7) Assemble the final flag

The challenge wants:

nite{attacker_ip_attacker_CID_flag_part_2}

From the decrypted HTTP/3 requests:

  • attacker IP: 192.0.2.66 (the client requesting /source and /flag)
  • attacker CID: 2457ce19cb87e0eb
  • flag part 2: qu1c_d4t4gr4m_pwn3d

So the final flag is:

nite{192.0.2.66_2457ce19cb87e0eb_qu1c_d4t4gr4m_pwn3d}