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.

3 min read
ibrahimsql
558 words

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:

  1. Find the TCP conversation carrying a scan — one source sending a large burst of connection attempts at a single destination.
  2. 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.
  3. 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 scapy
Python
from 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))
Text
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') 2

One 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.

Python
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.

Text
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 — SSH
  • 80/tcp — HTTP

5. Flag

KCTF{2}
---
Share this post:

What do you think?

React to show your appreciation

Related Posts