Step 158. Packet Sniffing Advanced — The Two Faces of Filters and Reassembling Conversations
Level 2 — Introduction to Security and Attack Skill Basics | Difficulty ★★★☆☆ | Estimated time: 3 hours
Prerequisites: Steps 83~85 (capture, filters, HTTP/HTTPS comparison) and Step 157 (plaintext credential exposure) complete. You can read pcaps with tshark.
- What you need: WSL Linux + tshark, Python 3 + Flask (for the plaintext login server), 2 terminals.
- ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime. Today’s capture range is inside your own computer only (
lo, loopback). - Measurement note: every output in this chapter was measured on 2026-09-09 on WSL Linux (TShark 4.2.2, Flask 3.1, curl 8.5).
So far you’ve learned the basics of "capture, filter, read packets." Today you learn three analyst skills — distinguishing capture filters from display filters (filtering while catching vs. viewing after catching), TCP stream reassembly (threading scattered packets into one conversation), and conversation statistics (drawing a map of who talked to whom, and how much).
With these three in place, large captures stop being scary. Pulling exactly "the one login" out of tens of thousands of packets and reading that conversation whole from start to finish — that’s today’s goal. At the end, you’ll deliberately cause a plaintext HTTP login and confirm the password reads out whole with a single stream reassembly.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Explain the difference between capture filters (BPF syntax) and display filters (Wireshark syntax), and use each correctly
- Search contents and flags with
tcp containsandtcp.flagscombinations - Reassemble scattered packets into one conversation with Follow TCP Stream
- Draw a communication map with Conversations statistics
- Extract plaintext-login credentials from a stream and explain the risk it implies
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | WSL Linux terminal, tshark (command-line Wireshark), a simple Flask server |
| Today’s commands | tshark -f (capture filter), -Y (display filter), -z follow,tcp,ascii,N, -z conv,tcp, tcp contains |
| Concepts needed | BPF syntax vs. display-filter syntax, TCP streams, session reassembly, Step 84’s flags |
| Today’s artifact | sniff.pcap + an analysis note — the complete reconstruction of one specific conversation |
2-1. Filters Come in Two Kinds — The Filtering Point Differs
The same word "filter" splits into two in Wireshark.
| Category | Capture filter | Display filter |
|---|---|---|
| Timing | Filters from the moment of capture | Filters at display time, from what’s caught |
| Syntax | BPF (tcp port 80, host 10.0.0.1) |
Wireshark (tcp.port==80, http) |
| tshark option | -f "..." |
-Y "..." |
| Use | Saving space on large volumes | Narrowing your view during analysis |
| Caution | Filtered packets aren’t in the file — irreversible | Original is intact — release the filter anytime |
The rule is this — only "things you can afford to throw away" go in the capture filter; everything else goes in the display filter. Capture filters are powerful, but what they filter is gone forever.
2-2. TCP Streams — Packets Are Fragments, a Stream Is a Conversation
The packet list is a pile of envelopes. The actual conversation (an HTTP request and response, a login exchange) rides split across several packets. Follow TCP Stream threads the fragments of one connection in order and shows you the "full transcript" — the most-used feature in analysis. In tshark, -z follow,tcp,ascii,streamnumber corresponds to it.
2-3. Conversations — Read the Map First
When there are tens of thousands of packets, reading line by line isn’t navigation. Conversations (conversation statistics) is a map summarizing the entire capture as "who talked to whom, how many packets, how many bytes." The field order is: first mark the strange conversations here (a large transfer at dawn, a counterparty you’ve never seen), then dig into just those conversations via streams.
2-4. Today’s Stage — A Loopback Login Server
For safe practice, we run a plaintext login server that lives only inside your computer. Since the capture range is lo (loopback) only, other people’s traffic physically cannot mix in.
3. Follow Along
3-1. Setting the Stage — A Plaintext Login Server and a Capture
Create a simple server (sniff_lab.py) in WSL.
from flask import Flask, request
app = Flask(__name__)
@app.route("/")
def index():
return "sniff-lab index page\n"
@app.route("/login", methods=["POST"])
def login():
return f"login try: {request.form.get('username')} / {request.form.get('password')}\n"
if __name__ == "__main__":
app.run(host="127.0.0.1", port=8000)
Start the server in terminal 1, and in terminal 2 start a capture with a capture filter — the capture filter‘s (-f) first appearance.
tshark -i lo -f "tcp port 8000" -a duration:8 -w sniff.pcap
While the capture runs, perform three actions in terminal 3 — view the page twice, log in once.
curl -s http://127.0.0.1:8000/ > /dev/null
curl -s -X POST -d "username=admin&password=sup3r-secret-pw" http://127.0.0.1:8000/login > /dev/null
curl -s http://127.0.0.1:8000/ > /dev/null
Output (measured 2026-09-09, part of the capture summary):
1 0.000000000 127.0.0.1 → 127.0.0.1 TCP 74 48258 → 8000 [SYN] ...
4 0.000096420 127.0.0.1 → 127.0.0.1 HTTP 143 GET / HTTP/1.1
16 0.005894011 127.0.0.1 → 127.0.0.1 HTTP 257 POST /login HTTP/1.1 (application/x-www-form-urlencoded)
28 0.012230354 127.0.0.1 → 127.0.0.1 HTTP 143 GET / HTTP/1.1
(36 packets total)
How to read it: thanks to the capture filter, only port-8000 conversations entered the file from the start — noise packets never existed. Inside 36 packets you can see three connections (three sets of three-way handshakes).
3-2. Display Filter 1 — Counting Only New Connections
Now view the captured file with a display filter (-Y). Just the conversation openers (SYN and not ACK):
tshark -r sniff.pcap -Y "tcp.flags.syn == 1 and tcp.flags.ack == 0"
Output (measured 2026-09-09):
1 0.000000000 127.0.0.1 → 127.0.0.1 TCP 74 48258 → 8000 [SYN] ...
13 0.005835323 127.0.0.1 → 127.0.0.1 TCP 74 48272 → 8000 [SYN] ...
25 0.012175168 127.0.0.1 → 127.0.0.1 TCP 74 48280 → 8000 [SYN] ...
How to read it: 3 out of 36 — the three connection starts picked out exactly. The combination filter you learned in Step 84 is used as-is. Display filters don’t touch the file, so apply and release them to your heart’s content.
3-3. Display Filter 2 — Searching by Content
Find directly "the packet carrying the string password."
tshark -r sniff.pcap -Y 'tcp contains "password"'
Output (measured 2026-09-09):
16 0.005894011 127.0.0.1 → 127.0.0.1 HTTP 257 POST /login HTTP/1.1 (application/x-www-form-urlencoded)
How to read it: just one out of 36 — only the login request packet was caught. tcp contains is a search through payload bytes, the standard investigation technique for finding "where did the credentials ride." Even on huge files, this one line starts the investigation.
Pulling only the body field:
tshark -r sniff.pcap -Y "http.request.method == POST" -T fields -e frame.number -e http.request.uri -e http.file_data
Output (measured 2026-09-09):
16 /login 757365726e616d653d61646d696e2670617373776f72643d73757033722d7365637265742d7077
Decode that hex and you get username=admin&password=sup3r-secret-pw — confirmed visually in the next section.
3-4. Follow TCP Stream — Reassembling the Full Conversation
Thread the entire connection (stream 1) that packet 16 belongs to.
tshark -r sniff.pcap -q -z follow,tcp,ascii,1
Output (measured 2026-09-09):
===================================================================
Follow: tcp,ascii
Filter: tcp.stream eq 1
Node 0: 127.0.0.1:48272
Node 1: 127.0.0.1:8000
191
POST /login HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: curl/8.5.0
Accept: */*
Content-Length: 39
Content-Type: application/x-www-form-urlencoded
username=admin&password=sup3r-secret-pw
173
HTTP/1.1 200 OK
Server: Werkzeug/3.1.8 Python/3.12.3
...
login try: admin / sup3r-secret-pw
===================================================================
How to read it: request (191 bytes) and response (173 bytes) stitched together in conversation order. And look — the ID and password in plaintext, printed once in the request body and once more in the server’s response body. In GUI Wireshark, right-click a packet → Follow → TCP Stream opens the same result in a window (Screen example). If the stream number differs, find yours by changing the N in tcp.stream eq N.
3-5. Conversations — The Communication Map
The conversation list for the whole capture.
tshark -r sniff.pcap -q -z conv,tcp
Output (measured 2026-09-09):
TCP Conversations
Filter:<No Filter>
| <- | | -> | | Total | Relative | Duration |
| Frames Bytes | | Frames Bytes | | Frames Bytes | Start | |
127.0.0.1:48258 <-> 127.0.0.1:8000 5 532 bytes 7 547 bytes 12 1079 bytes 0.000000 0.0018
127.0.0.1:48272 <-> 127.0.0.1:8000 5 546 bytes 7 661 bytes 12 1207 bytes 0.005835 0.0015
127.0.0.1:48280 <-> 127.0.0.1:8000 5 532 bytes 7 547 bytes 12 1079 bytes 0.012175 0.0019
How to read it: three conversations, 12 packets each. Only the second conversation (48272) has subtly different bytes (1207 vs 1079) — the conversation carrying the login data. In the field, you first mark in this table "conversations with outlier bytes, conversations at strange times." The order is: look at the map, then walk into the alley (the stream).
3-6. A Syntax-Confusion Demo — Putting Display Syntax in a Capture Filter
What happens when you mix the two syntaxes? Let’s get it wrong on purpose. Put display-filter syntax (tcp.port==80) into the capture filter (-f):
tshark -i lo -f "tcp.port==80" -c 1
Output (measured 2026-09-09):
tshark: Invalid capture filter "tcp.port==80" for interface 'Loopback'.
How to read it: BPF syntax is tcp port 80 (no ==); display-filter syntax is tcp.port==80. Mix them and you get rejected like this before capture even starts. When this error appears, it means you picked the wrong syntax — check first whether the option is -f or -Y. One last habit: don’t delete the sniff.pcap you made today — keep it. Saving a capture and reopening it later is the basic workflow of real analysis.
4. Missions & Exercises
Mission — Complete Reconstruction of One Conversation
- Reproduce 3-1’s server and capture to make
sniff.pcap(capture filter required) - Find the one login packet with a display filter —
tcp contains "password"or a search term of your own - Reassemble the stream that packet belongs to with
follow,tcp,ascii, and quote the full request/response transcript in your note - Find that conversation’s row in the Conversations output and write "which column hints it’s the login"
- Closing paragraph of the analysis note: "what would have differed if I hadn’t filtered at capture, and what would I have lost if I’d filtered wrong at capture"
Exercises
Exercise 1. Explain the difference between capture filters and display filters along two axes: "the filtering point" and "is it reversible."
Exercise 2. Write where each of tcp port 80 and tcp.port==80 belongs, and what happens if you mix them.
Exercise 3. Explain why Follow TCP Stream is more powerful than "viewing the packet list," connecting it to the structure of an HTTP conversation (request-response order and fragmentation).
Exercise 4. In 3-4 you confirmed the password was printed twice — in request and response. Explain how this experiment goes beyond "why you shouldn’t reuse passwords on plaintext services" to show "why a server must never echo a password back in a response."
5. Model Answers & Completion Criteria
Mission Model Answer
Example quote for item 3 (measured 2026-09-09):
Stream 1 reassembly result:
request → POST /login, body username=admin&password=sup3r-secret-pw (plaintext)
response → 200 OK, body "login try: admin / sup3r-secret-pw" (plaintext re-exposure)
Example answer for item 4: "In Conversations, the 48272 conversation’s Total is 1207 bytes, larger than the other conversations (1079) — the bytes grew because the login form data (39 bytes) and the response phrase rode along. A subtle byte-count difference becomes a clue to ‘which conversation carries content.’"
Example paragraph for item 5: "Had I captured without a capture filter, other loopback traffic (internal DNS, etc.) would have mixed in, raising the cost of finding the target conversation. Conversely, had I wrongly set the capture filter to something like tcp port 22, the login packets wouldn’t be in the file at all — unrecoverable by any display filter. A capture filter is deletion — filter only what you can afford to throw away."
How to verify: ① was the pcap actually created and was a -f filter used? ② does the stream-transcript quote contain both request and response? ③ do the Conversations row and the stream point to the same conversation (cross-check by port number)? ④ does the paragraph reach "irreversibility"?
Exercise Answers
Answer 1. A capture filter filters before packets are saved, so filtered packets don’t exist in the file — an irreversible deletion. A display filter filters only at display time on an already-saved file, so releasing the filter shows everything again — a reversible view adjustment. That’s why only certain noise goes in the capture filter, and analysis-time narrowing goes in the display filter.
Answer 2. tcp port 80 is BPF syntax, used in the capture filter (-f); tcp.port==80 is Wireshark display-filter syntax (-Y or the GUI filter box). Mixed, you get rejected before capture with Invalid capture filter (confirmed by measurement), or a syntax-error mark on the display-filter side. Looking at the option letter (-f/-Y) tells you which syntax to use.
Answer 3. The packet list mixes every connection in time order, so one conversation’s request and response scatter across the list — and large bodies fragment into several packets. Follow TCP Stream gathers only the fragments of one connection and threads them in sequence order, so you can read on one screen the whole story of "what the client sent and what the server answered." It’s the feature that raises the unit of analysis from packets to conversations.
Answer 4. Apart from the risk of password reuse, a server that echoes a received password in its response body doubles the exposure points — miss the request and it still reads from the response; it gets left twice in every log, proxy, and capture. A password should exist only on its way to the server (the request), and the server must never print or store it again (verify by hash only). Today’s experiment server is a deliberate counterexample showing that "thing you must not do."
Completion Criteria Checklist
- [ ] I can explain the capture filter / display filter difference (timing, syntax, reversibility)
- [ ] I use the right syntax for
-fand-Y - [ ] I can search contents with
tcp contains - [ ] I reassembled a conversation with Follow TCP Stream (
-z follow,tcp,ascii,N) - [ ] I drew a conversation map with Conversations (
-z conv,tcp) - [ ] I confirmed plaintext credential exposure in a capture and can explain it
- [ ] Mission: complete reconstruction note of one conversation done
6. Common Pitfalls & Fixes
Wall 1. I get the error tshark: Invalid capture filter "tcp.port==80"
Cause: you put display-filter syntax in the capture-filter slot (-f) (message measured 2026-09-09).
Fix: capture filters use BPF syntax — tcp port 80 (no equals sign). If you want display-filter syntax (tcp.port==80), switch the option to -Y. The hint is that the error message says "capture filter."
Wall 2. The packets I want aren’t in the capture file
Cause: the capture filter was too narrow, or the capture started after the action. Filtered packets are not recovered.
Fix: at first, practice capturing wide (no filter, or just -i lo) and narrowing with display filters. Capture filters are for "when you’re sure what you’re catching." And start the capture before performing the action (curl).
Wall 3. I don’t know the stream number for follow
Symptom: you ran -z follow,tcp,ascii,0 and a different conversation came out.
Cause: stream numbers run 0, 1, 2…, so you need to know which conversation your target packet belongs to.
Fix: from the target packet number, first get the stream number with tshark -r sniff.pcap -Y "frame.number==16" -T fields -e tcp.stream. Or the conversation order in -z conv,tcp corresponds to stream numbers.
Wall 4. tcp contains catches nothing
Cause: the string you’re looking for actually rides differently — case, URL encoding (%40, etc.), compression/encryption (HTTPS).
Fix: first view the conversation with an http filter and check the actual bytes. On HTTPS, content search is fundamentally impossible — that’s encryption’s job (Step 85).
Wall 5. tshark’s warning message bothers me — Running as user "root"...
Cause: the standard warning from running as root in WSL. Capture tools need elevated privileges to see packets; for today’s practice (loopback only), it’s an informational warning you can ignore.
Fix: for clean output, discard stderr with 2>/dev/null. That said, on real servers, resident captures running as root are to be avoided as a principle.
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| Capture filter (BPF) | Filters from the moment of capture — what’s discarded is unrecoverable |
| Display filter | Filters at display time from what’s caught — the original is safe |
tcp contains |
Payload content search — the start of credential hunting |
| Follow TCP Stream | Reassembles scattered fragments into a full conversation transcript |
| Conversations | Who talked to whom, how much — the analyst’s map |
| Session reconstruction | The rise from packet-unit analysis to conversation-unit analysis |
Today’s Commands & Tools
| Command/filter | What it does |
|---|---|
tshark -i lo -f "tcp port 8000" -w f.pcap |
Captures only port 8000 (BPF) |
tshark -r f.pcap -Y "tcp.flags.syn==1 and tcp.flags.ack==0" |
Shows only new connections (openers) |
tshark -r f.pcap -Y 'tcp contains "password"' |
Searches contents for a string |
tshark -r f.pcap -q -z follow,tcp,ascii,1 |
Full transcript of stream 1 |
tshark -r f.pcap -q -z conv,tcp |
Conversation-statistics map |
-T fields -e tcp.stream |
Checks a packet’s stream number |
An Instinct More Important Than Commands
Today’s practice looks like tool usage on the surface, but inside, you learned "the analyst’s order of gaze" — look at the map (Conversations), narrow into the alley (filters), and enter the room (the stream). That order is the same when the capture is a million packets. And remember the final scene — that screen where a plaintext password lies on a single stream line. Tools that can show that screen are everywhere in the world, and that’s why the world changed its default to HTTPS. Today you became someone who has seen with their own eyes what the world looked like before that default existed.
Once every box is checked, Step 158 is complete. Click the checkbox in the sidebar to save your progress.