# CTF Forensics - Network
## Table of Contents
- [tcpdump Quick Reference](#tcpdump-quick-reference)
- [TLS/SSL Decryption via Keylog File](#tlsssl-decryption-via-keylog-file)
- [Wireshark Basics](#wireshark-basics)
- [Port Scan Analysis](#port-scan-analysis)
- [Gateway/Device via MAC OUI](#gatewaydevice-via-mac-oui)
- [WordPress Reconnaissance](#wordpress-reconnaissance)
- [Post-Exploitation Traffic](#post-exploitation-traffic)
- [Credential Extraction](#credential-extraction)
- [SMB3 Encrypted Traffic](#smb3-encrypted-traffic)
- [5G/NR Protocol Analysis](#5gnr-protocol-analysis)
- [Email Headers](#email-headers)
- [USB HID Stenography/Chord PCAP (UTCTF 2024)](#usb-hid-stenographychord-pcap-utctf-2024)
- [BCD Encoding in UDP (VuwCTF 2025)](#bcd-encoding-in-udp-vuwctf-2025)
- [HTTP File Upload Exfiltration in PCAP (MetaCTF 2026)](#http-file-upload-exfiltration-in-pcap-metactf-2026)
- [Packet Interval Timing-Based Encoding (EHAX 2026)](#packet-interval-timing-based-encoding-ehax-2026)
- [USB HID Mouse/Pen Drawing Recovery (EHAX 2026)](#usb-hid-mousepen-drawing-recovery-ehax-2026)
- [NTLMv2 Hash Cracking from PCAP (Pragyan 2026)](#ntlmv2-hash-cracking-from-pcap-pragyan-2026)
- [TCP Flag Covert Channel (BearCatCTF 2026)](#tcp-flag-covert-channel-bearcatctf-2026)
- [DNS Query Name Last-Byte Steganography (UTCTF 2026)](#dns-query-name-last-byte-steganography-utctf-2026)
- [Multi-Layer PCAP with XOR + ZIP (UTCTF 2026)](#multi-layer-pcap-with-xor--zip-utctf-2026)
- [Brotli Decompression Bomb Seam Analysis (BearCatCTF 2026)](#brotli-decompression-bomb-seam-analysis-bearcatctf-2026)
---
## tcpdump Quick Reference
Command-line packet capture tool for quick network forensics triage.
```bash
# Basic capture on interface
sudo tcpdump -i eth0
# Capture to file
sudo tcpdump -i eth0 -w capture.pcap
# Filter by source IP
sudo tcpdump -i eth0 src 192.168.1.100
# Filter by destination port
sudo tcpdump -i eth0 dst port 80
# Combined filter with file output
sudo tcpdump -i eth0 -w packets.pcap 'src 172.22.206.250 and port 443'
# Read from file with verbose output
tcpdump -r capture.pcap -v
# Show packet contents in ASCII
tcpdump -r capture.pcap -A
# Show hex + ASCII dump
tcpdump -r capture.pcap -X
# Count total packets
tcpdump -r capture.pcap -q | wc -l
```
**Common filters:**
| Filter | Description |
|--------|-------------|
| `host 10.0.0.1` | Traffic to/from IP |
| `net 192.168.1.0/24` | Entire subnet |
| `port 80` | HTTP traffic |
| `tcp` / `udp` / `icmp` | Protocol filter |
| `src host X and dst port Y` | Combined |
**Key insight:** Use tcpdump for quick command-line triage when Wireshark is unavailable. Pipe to `strings` or `grep` for fast flag hunting: `tcpdump -r capture.pcap -A | grep -i flag`.
---
## TLS/SSL Decryption via Keylog File
To decrypt TLS traffic in Wireshark, provide either the pre-master secret or a keylog file.
**Method 1 — SSLKEYLOGFILE (client-side key logging):**
If the challenge provides a keylog file (or you can set `SSLKEYLOGFILE`):
```bash
# Set environment variable before running the client
export SSLKEYLOGFILE=/tmp/sslkeys.log
curl https://target/secret
# Import into Wireshark:
# Edit → Preferences → Protocols → TLS → (Pre)-Master-Secret log filename → /tmp/sslkeys.log
```
**Keylog file format (NSS Key Log Format):**
```text
CLIENT_RANDOM <32_bytes_client_random_hex> <48_bytes_master_secret_hex>
```
**Method 2 — RSA private key (if server key is known):**
**Note:** Only works with RSA key exchange. Sessions using forward secrecy (ECDHE/DHE cipher suites) cannot be decrypted with the server's private key — use Method 1 instead. CTF challenges with weak RSA keys typically use RSA key exchange.
```bash
# Wireshark: Edit → Preferences → Protocols → TLS → RSA keys list
# IP: 127.0.0.1, Port: 443, Protocol: http, Key File: server.key
# Or via tshark:
tshark -r capture.pcap -o "tls.keys_list:127.0.0.1,443,http,server.key" -Y http
```
**Method 3 — Weak RSA key factoring (see also linux-forensics.md):**
```bash
# Extract certificate from PCAP
tshark -r capture.pcap -Y "tls.handshake.type==11" -T fields -e tls.handshake.certificate | head -1
# Factor weak modulus, generate private key with rsatool
python rsatool.py -p
-q -e 65537 -o server.key
# Import key into Wireshark
```
**SSL handshake components needed for decryption:**
1. `client_random` — sent in ClientHello
2. `server_random` — sent in ServerHello
3. Pre-master secret (PMS) — encrypted in ClientKeyExchange with server's RSA public key
**Key insight:** Look for keylog files (`.log`, `sslkeys.txt`) in challenge artifacts. If the challenge gives you a private key, use it directly. For weak RSA keys in certificates, factor the modulus to derive the private key.
---
## Wireshark Basics
```bash
# Filters
http.request.method == "POST"
tcp.stream eq 5
frame contains "flag"
# Export files
File → Export Objects → HTTP
# tshark
tshark -r capture.pcap -Y "http" -T fields -e http.file_data
tshark -r capture.pcap --export-objects http,/tmp/http_objects
```
---
## Port Scan Analysis
```bash
# IP conversation statistics
tshark -r capture.pcap -q -z conv,ip
# Find open ports (SYN-ACK responses)
tshark -r capture.pcap -Y "tcp.flags.syn==1 && tcp.flags.ack==1" \
-T fields -e ip.src -e tcp.srcport | sort -u
```
---
## Gateway/Device via MAC OUI
```bash
# Extract MAC addresses
tshark -r capture.pcap -Y "arp" -T fields \
-e arp.src.hw_mac -e arp.src.proto_ipv4 | sort -u
# Vendor lookup
curl -s "https://macvendors.com/query/88:bd:09"
```
---
## WordPress Reconnaissance
**Identify WPScan:**
```bash
tshark -r capture.pcap -Y "http.user_agent contains \"WPScan\"" | head -1
```
**WordPress version:**
```bash
cat /tmp/http_objects/feed* | grep -i generator
```
**Plugins:**
```bash
tshark -r capture.pcap \
-Y "http.response.code == 200 && http.request.uri contains \"wp-content/plugins\"" \
-T fields -e http.request.uri | sort -u
```
**Usernames (REST API):**
```bash
cat /tmp/http_objects/*per_page* | jq '.[].name'
```
---
## Post-Exploitation Traffic
**Step 1: TCP conversations**
```bash
tshark -r capture.pcap -q -z conv,tcp
```
**Step 2: Established connections (SYN-ACK)**
```bash
tshark -r capture.pcap -Y "tcp.flags.syn == 1 and tcp.flags.ack == 1" \
-T fields -e ip.src -e ip.dst -e tcp.srcport -e tcp.dstport | sort -u
```
**Step 3: Follow TCP stream**
```bash
tshark -r capture.pcap -q -z "follow,tcp,ascii,"
```
**Reverse shell indicators:**
- `bash: cannot set terminal process group`
- `bash: no job control in this shell`
- Shell prompts like `www-data@hostname:/path$`
---
## Credential Extraction
**High-value files:**
| Application | File | Format |
|-------------|------|--------|
| WordPress | `wp-config.php` | `define('DB_PASSWORD', '...')` |
| Laravel | `.env` | `DB_PASSWORD=` |
| MySQL | `/etc/mysql/debian.cnf` | `password = ` |
```bash
# Search shell stream for credentials
tshark -r capture.pcap -q -z "follow,tcp,ascii," | grep -i "password"
```
---
## SMB3 Encrypted Traffic
**Step 1: Extract NTLMv2 hash**
```bash
tshark -r capture.pcap -Y "ntlmssp.messagetype == 0x00000003" -T fields \
-e ntlmssp.ntlmv2_response.ntproofstr \
-e ntlmssp.auth.username
```
**Step 2: Crack with hashcat**
```bash
hashcat -m 5600 ntlmv2_hash.txt wordlist.txt
```
**Step 3: Derive SMB 3.1.1 session keys (Python)**
```python
from Cryptodome.Cipher import AES, ARC4
from Cryptodome.Hash import MD4
import hmac, hashlib
def SP800_108_Counter_KDF(Ki, Label, Context, L):
n = (L // 256) + 1
result = b''
for i in range(1, n + 1):
data = i.to_bytes(4, 'big') + Label + b'\x00' + Context + L.to_bytes(4, 'big')
result += hmac.new(Ki, data, hashlib.sha256).digest()
return result[:L // 8]
# Compute session key
nt_hash = MD4.new(password.encode('utf-16le')).digest()
response_key = hmac.new(nt_hash, (user.upper() + domain.upper()).encode('utf-16le'), hashlib.md5).digest()
key_exchange_key = hmac.new(response_key, ntproofstr, hashlib.md5).digest()
session_key = ARC4.new(key_exchange_key).encrypt(encrypted_session_key)
# Derive encryption keys
c2s_key = SP800_108_Counter_KDF(session_key, b"SMBC2SCipherKey\x00", preauth_hash, 128)
s2c_key = SP800_108_Counter_KDF(session_key, b"SMBS2CCipherKey\x00", preauth_hash, 128)
```
**Step 4: Decrypt (AES-128-GCM)**
```python
def decrypt_smb311(transform_data, key):
signature = transform_data[4:20]
nonce = transform_data[20:32]
aad = transform_data[20:52]
encrypted = transform_data[52:]
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
cipher.update(aad)
return cipher.decrypt_and_verify(encrypted, signature)
```
---
## 5G/NR Protocol Analysis
**Wireshark setup:**
- Enable: NAS-5GS, RLC-NR, PDCP-NR, MAC-NR
**SMS in 5G (3GPP TS 23.040):**
| IEI | Format |
|-----|--------|
| 0x0c | iMelody (ringtone) |
| 0x0e | Large Animation (16×16) |
| 0x18 | WVG (vector graphics) |
**iMelody to Morse:**
- Notes like `c4c4c4r2` encode dots/dashes
---
## Email Headers
- Check routing information
- Look for encoded attachments (base64)
- MIME boundaries may hide data
---
## USB HID Stenography/Chord PCAP (UTCTF 2024)
**Pattern (Gibberish):** USB keyboard PCAP with simultaneous multi-key presses = stenography chording.
**Detection:** Multiple simultaneous USB HID keys (6+ at once) in interrupt transfers. Not regular typing.
**Decoding workflow:**
1. Extract HID reports from PCAP
2. Detect simultaneous key states (multiple keycodes in same report)
3. Map chords to Plover stenography dictionary
4. Install Plover, use its dictionary for translation
```bash
# Extract USB HID data
tshark -r capture.pcap -Y "usb.transfer_type == 1" -T fields -e usb.capdata
```
---
## BCD Encoding in UDP (VuwCTF 2025)
**Pattern (1.5x-engineer):** "1.5x" hints at the encoding ratio.
**BCD (Binary-Coded Decimal):** Each nibble (4 bits) encodes one decimal digit (0-9). Two digits per byte vs one ASCII digit per byte → BCD is 2x denser than ASCII decimal. The "1.5x" name refers to the challenge-specific framing: 3 BCD bytes encode 6 digits which represent 2 ASCII bytes (3:2 ratio).
**Decoding:**
```python
def bcd_decode(data):
result = ''
for byte in data:
high = (byte >> 4) & 0x0F
low = byte & 0x0F
result += f'{high}{low}'
return result
# UDP sessions differentiated by first byte
# Session 1 = BCD-encoded ASCII metadata with flag
# Session 2 = encrypted DOCX
```
**Lesson:** Challenge name often hints at encoding ratio or technique.
---
## HTTP File Upload Exfiltration in PCAP (MetaCTF 2026)
**Pattern (Dead Drop):** Small PCAP with TCP streams containing HTTP traffic. Exfiltrated data uploaded as a file via multipart form POST.
**Quick triage:**
```bash
# Count packets and protocols
tshark -r capture.pcap -q -z io,phs
# List HTTP requests
tshark -r capture.pcap -Y "http.request" -T fields -e http.request.method -e http.request.uri -e http.host
# Export all HTTP objects (files transferred)
tshark -r capture.pcap --export-objects http,/tmp/http_objects
ls -la /tmp/http_objects/
# Follow specific TCP streams
tshark -r capture.pcap -q -z "follow,tcp,ascii,0"
tshark -r capture.pcap -q -z "follow,tcp,ascii,1"
```
**Extraction workflow:**
1. Export HTTP objects — uploaded files are extracted automatically
2. Check for multipart form-data POST requests (file uploads)
3. Look for unusual User-Agent strings (e.g., `DeadDropBot/1.0`) indicating automated exfiltration
4. Extracted files may be images (PNG/JPEG) with flag text rendered visually — open and inspect
**Key indicators of exfiltration:**
- POST to `/upload` endpoints
- Non-standard User-Agent strings
- Small number of packets but containing file transfers
- "Dead drop" pattern: attacker uploads file to web server for later retrieval
**Lesson:** Always start with `--export-objects` to extract transferred files before deep packet analysis. The flag is often in the exfiltrated file itself.
---
## Packet Interval Timing-Based Encoding (EHAX 2026)
**Pattern (Breathing Void):** Large PCAPNG with millions of packets, but only a few hundred on one interface carry data. The signal is in the **timing gaps** between identical packets, not their content.
**Identification:** Challenge mentions "breathing", "void", "silence", or timing. PCAP has many interfaces but only one has interesting traffic. Packets are identical but spaced at two distinct intervals.
**Decoding workflow:**
```python
from scapy.all import rdpcap
packets = rdpcap('challenge.pcapng')
# 1. Filter to the right interface (e.g., interface 2)
# tshark: tshark -r challenge.pcapng -Y "frame.interface_id == 2" -T fields -e frame.time_epoch
# 2. Compute inter-packet intervals
times = [float(pkt.time) for pkt in packets if pkt.sniffed_on == 'interface_2']
intervals = [times[i+1] - times[i] for i in range(len(times)-1)]
# 3. Identify binary mapping (two distinct interval values)
# E.g., 10ms → 0, 100ms → 1 (threshold at ~50ms)
threshold = 0.05 # 50ms
bits = [0 if dt < threshold else 1 for dt in intervals]
# 4. May need to prepend a leading 0 bit (first interval has no predecessor)
bits = [0] + bits
# 5. Convert bits to bytes (MSB-first)
data = bytes(int(''.join(str(b) for b in bits[i:i+8]), 2)
for i in range(0, len(bits) - 7, 8))
print(data.decode(errors='replace'))
```
**Key insight:** When identical packets appear on a single interface with only two practical interval values, it's almost certainly binary encoding via timing. The content is noise — the signal is in the gaps. Filter by interface and count unique intervals first.
**Scale tip:** Large PCAPs (millions of packets) often have the signal in a tiny subset. Triage with `tshark -q -z io,phs` to find which interface has the fewest packets — that's likely the data carrier.
---
## USB HID Mouse/Pen Drawing Recovery (EHAX 2026)
**Pattern (Painter):** PCAP contains USB HID interrupt transfers from a mouse/pen device. Drawing data encoded as relative movements with multiple draw modes.
**Packet format (7-byte HID reports):**
| Byte | Field | Notes |
|------|-------|-------|
| 0 | Button state | 0x01 = pressed (may be constant) |
| 1 | Mode/pad | 0=hover, 1=draw mode 1, 2=draw mode 2 |
| 2-3 | dx (int16 LE) | Relative X movement |
| 4-5 | dy (int16 LE) | Relative Y movement |
| 6 | Wheel | Usually 0 |
**Extraction and rendering:**
```python
import struct
from PIL import Image, ImageDraw
# Extract HID data
# tshark -r capture.pcap -Y "usb.transfer_type==1" -T fields -e usb.capdata
packets = []
with open('hid_data.txt') as f:
for line in f:
raw = bytes.fromhex(line.strip().replace(':', ''))
if len(raw) >= 7:
btn = raw[0]
mode = raw[1]
dx = struct.unpack(' capdata.txt
awk '
function hexval(c){ return index("0123456789abcdef",tolower(c))-1 }
function hex2dec(h, n,i){ n=0; for(i=1;i<=length(h);i++) n=n*16+hexval(substr(h,i,1)); return n }
function s16(u){ return (u>=32768)?u-65536:u }
{ d=$1; if(length(d)!=14) next
btn=hex2dec(substr(d,3,2))
x=s16(hex2dec(substr(d,7,2) substr(d,5,2)))
y=s16(hex2dec(substr(d,11,2) substr(d,9,2)))
print btn, x, y }' capdata.txt > deltas.txt
```
Then render with SVG (Python) — filter on pen-down state (button=2), accumulate deltas, flip Y axis, draw strokes between consecutive pen-down points.
**Difference from keyboard HID:** Mouse HID uses relative movements (accumulated), keyboard uses keycodes (direct). Mouse drawing requires rendering; keyboard requires keymap lookup.
---
## NTLMv2 Hash Cracking from PCAP (Pragyan 2026)
**Pattern ($whoami):** SMB2 authentication in packet capture.
**Extraction:** From NTLMSSP_AUTH packet, extract: server challenge, NTProofStr, and blob.
**Brute-force with known password format:**
```python
import hashlib, hmac
from Crypto.Hash import MD4
def try_password(password, username, domain, server_challenge, blob, expected_proof):
nt_hash = MD4.new(password.encode('utf-16-le')).digest()
identity = (username.upper() + domain).encode('utf-16-le')
ntlmv2_hash = hmac.new(nt_hash, identity, hashlib.md5).digest()
proof = hmac.new(ntlmv2_hash, server_challenge + blob, hashlib.md5).digest()
return proof == expected_proof
```
---
## TCP Flag Covert Channel (BearCatCTF 2026)
**Pattern (pCapsized):** Suspicious TCP packets with chaotic flag combinations (FIN+SYN, SYN+RST+PSH+URG, etc.). The 6 TCP flag bits encode base64 characters.
**Decoding:**
```python
from scapy.all import rdpcap, TCP
pkts = rdpcap('capture.pcap')
suspicious = [p for p in pkts if TCP in p and p[TCP].dport == 5748]
# Map 6-bit flag value to base64 alphabet
b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
encoded = ''.join(b64[p[TCP].flags & 0x3F] for p in suspicious)
import base64
flag = base64.b64decode(encoded).decode()
```
**Key insight:** TCP has 6 standard flag bits (FIN, SYN, RST, PSH, ACK, URG) = values 0-63, matching the base64 alphabet exactly. Unusual flag combinations on otherwise normal-looking packets indicate covert channel usage. Filter by destination port or source IP to isolate the channel.
**Detection:** Packets with nonsensical flag combinations (e.g., FIN+SYN simultaneously). Consistent destination port. Packet count is a multiple of 4 (base64 alignment).
---
## DNS Query Name Last-Byte Steganography (UTCTF 2026)
**Pattern (Last Byte Standing):** PCAP with DNS queries where data is encoded in the last byte of each query name.
**Identification:** Many DNS queries to unusual or sequential subdomains. The meaningful data is NOT in the query name itself but in the final byte/character of each name.
**Decoding workflow:**
```python
from scapy.all import rdpcap, DNS, DNSQR
packets = rdpcap('last-byte-standing.pcap')
data = []
for pkt in packets:
if pkt.haslayer(DNSQR):
qname = pkt[DNSQR].qname.decode(errors='replace').rstrip('.')
if qname:
data.append(qname[-1]) # Last character of query name
# Reconstruct message from last bytes
message = ''.join(data)
print(message)
# May need additional decoding (hex, base64, etc.)
```
**Variants:**
- Last byte of each subdomain label (split on `.`)
- Specific character position (first, Nth, last)
- Hex-encoded bytes across multiple queries
- Subdomain labels as base32/base64 chunks (DNS tunneling)
**Key insight:** DNS exfiltration often hides data in query names. When queries look random but follow a pattern, extract specific character positions. The "last byte" pattern is simple but effective — each query contributes one byte to the message.
**Detection:** Large number of DNS queries to a single domain, queries with no legitimate purpose, sequential or patterned subdomain names.
---
## Multi-Layer PCAP with XOR + ZIP (UTCTF 2026)
**Pattern (Half Awake):** Small PCAP containing data that requires multiple decoding layers: extract raw data from packets, XOR-decrypt, then decompress (ZIP/gzip).
**Workflow:**
```python
from scapy.all import rdpcap, Raw
packets = rdpcap('half-awake.pcap')
# 1. Extract raw payload data from TCP/UDP streams
raw_data = b''
for pkt in packets:
if pkt.haslayer(Raw):
raw_data += pkt[Raw].load
# 2. Try XOR with common single-byte keys
for key in range(256):
decrypted = bytes(b ^ key for b in raw_data)
# Check for known magic bytes
if decrypted[:2] == b'PK': # ZIP
with open(f'decrypted_{key}.zip', 'wb') as f:
f.write(decrypted)
print(f"ZIP found with XOR key {key:#04x}")
elif decrypted[:3] == b'\x1f\x8b\x08': # gzip
print(f"gzip found with XOR key {key:#04x}")
# 3. Extract and search for flag
import zipfile, io
zf = zipfile.ZipFile(io.BytesIO(decrypted))
for name in zf.namelist():
content = zf.read(name)
if b'flag' in content.lower():
print(f"{name}: {content}")
```
**Key insight:** Small PCAPs with seemingly random data often require multi-layer decoding. Try XOR brute-force first (only 256 keys for single-byte), then check for archive magic bytes in the decrypted output.
---
## Brotli Decompression Bomb Seam Analysis (BearCatCTF 2026)
**Pattern (Cursed Map):** HTTP download of a file that decompresses to gigabytes (decompression bomb). The flag is sandwiched between two bomb halves at a seam in the compressed data.
**Identification:** Compressed data shows a repeating block pattern (e.g., 105-byte period). One block breaks the pattern — the flag is at this discontinuity.
```python
import brotli
with open('flag.txt.br', 'rb') as f:
data = f.read()
# Find the repeating block size
block_size = 105 # Determined by comparing adjacent blocks
for i in range(0, len(data) - block_size, block_size):
if data[i:i+block_size] != data[i+block_size:i+2*block_size]:
seam_offset = i + block_size
break
# Decompress only the anomalous block
dec = brotli.Decompressor()
result = dec.process(data[seam_offset:seam_offset+block_size])
# Flag is in the decompressed output
```
**Key insight:** Decompression bombs use highly repetitive compressed data. The flag breaks this repetition, creating a detectable anomaly in the compressed stream. Compare adjacent fixed-size blocks to find the discontinuity, then decompress only that region — no need to decompress the entire multi-gigabyte output.
**Detection:** File with extreme compression ratio (MB → GB), HTTP Content-Encoding: br, or file identified as Brotli. Tools hang or OOM when trying to decompress.