KnightCTF 2026 — Reconnaissance Writeup
A full 65535-port TCP SYN sweep from 192.168.1.104 against 192.168.1.102: classify every response into SYN-ACK (open) versus RST (closed) and the target exposes exactly two open ports.
KnightCTF 2026 — Reconnaissance
First of a three-capture incident-response scenario. "Knight Blog", a mid-sized e-learning company, spots suspicious traffic and hands over packet captures. This capture is the reconnaissance phase.
- Event: KnightCTF 2026
- Category: Network / Forensics
- Question: how many ports were found to be open on the target?
- Flag format:
KCTF{number}
1. The capture
The archive contains a single file, pcap1.pcapng (about 7 MB, ~141k packets). The question only concerns this first stage, so the whole task is to classify one scan.
The scenario promises "suspicious scanning activity", so the plan is:
- Find the TCP conversation carrying a scan — one source sending a large burst of connection attempts at a single destination.
- Split the target's replies into open and closed: a port is open when it answers a SYN with SYN-ACK, and closed when it answers with RST.
- Count distinct open ports.
2. Reading the capture
tshark was not installed, so I used Scapy, which parses pcapng without any system libraries:
python3 -m venv venv && ./venv/bin/pip install scapyfrom scapy.all import rdpcap, TCP, IP
from collections import Counter
pkts = rdpcap("pcap1.pcapng")
tcp = [p for p in pkts if p.haslayer(TCP) and p.haslayer(IP)]
# SYN with no ACK = probe
syns = [p for p in tcp if (p[TCP].flags & 0x02) and not (p[TCP].flags & 0x10)]
# SYN + ACK = open port answering
synack = [p for p in tcp if (p[TCP].flags & 0x12) == 0x12]
print("packets:", len(pkts), "tcp:", len(tcp))
print(Counter((p[IP].src, p[IP].dst) for p in syns).most_common(3))packets: 141259 tcp: 135532
SYN (no ACK) packets: 65574
SYN-ACK packets: 39
top SYN sources:
('192.168.1.104', '192.168.1.102') 65558
('192.168.1.110', '192.168.1.102') 7
('192.168.1.103', '57.155.141.115') 2One conversation dominates: 192.168.1.104 sends 65,558 bare SYNs at 192.168.1.102. That is the scanner (192.168.1.104) sweeping the target (192.168.1.102); every other source is background traffic. The scanned set covers all 65,535 TCP ports.
3. Open vs closed
For the target's replies, only two flag combinations matter: SYN+ACK (0x12) means the port is open, RST (0x04) means it is closed.
scanner, target = "192.168.1.104", "192.168.1.102"
resp = {}
for p in tcp:
if p[IP].src == target and p[IP].dst == scanner:
port = p[TCP].sport
if (p[TCP].flags & 0x12) == 0x12:
resp[port] = "SYN-ACK"
elif p[TCP].flags & 0x04:
resp.setdefault(port, "RST")
open_ports = sorted(pt for pt, r in resp.items() if r == "SYN-ACK")
print("ports responded:", len(resp))
print("OPEN:", open_ports)ports responded: 65535
OPEN: [22, 80]Every one of the 65,535 probed ports replied, and exactly two replied with a SYN-ACK: SSH (22) and HTTP (80). Everything else returned RST. The large SYN-ACK count seen earlier (23 packets to the scanner) is just retransmission on those two ports — a SYN sweep never completes the handshake, so the target re-sends its SYN-ACK.
Cross-checking against the other, smaller conversations confirms it: every source that touched the target was answered on the same two ports, and no port outside {22, 80} ever returned anything other than RST.
SYN-ACK from 192.168.1.102 grouped by scanner:
(192.168.1.102, 192.168.1.104) -> [22, 80]
(192.168.1.102, 192.168.1.110) -> [80]
(192.168.1.102, 192.168.1.101) -> [80]
ALL open ports on target 192.168.1.102: [22, 80]4. Answer
The reconnaissance scan against 192.168.1.102 found 2 open ports:
22/tcp— SSH80/tcp— HTTP
5. Flag
KCTF{2}What do you think?
React to show your appreciation