Chapter 7

Display Filter Language

Wireshark's display filter language is a full expression system with protocol field access, comparison operators, membership testing, and regular expressions. Mastering it means you can isolate any pattern in a PCAP in seconds — from "all packets containing the string 'mimikatz'" to "all TLS connections with unusual cipher suites" to "all HTTP responses larger than 1 MB."

Scenario

A 1.8 GB capture file. You need to find: all SYN packets to establish which hosts were scanned, all DNS queries for high-entropy domains, all HTTP POST requests (potential data exfiltration), all TLS connections where the certificate's Common Name doesn't match the SNI (potential domain fronting), and all ICMP packets with payloads over 64 bytes (potential ICMP tunneling). Each requires a different display filter. You'll build them from first principles rather than Google-searching each time.

Core Syntax

  Display Filter Syntax
  ═══════════════════════════════════════════════════════════════════

  Basic form:   protocol.field  operator  value
  Example:      ip.dst          ==        185.220.101.47

  Protocol field access:
    ip.src, ip.dst          → IP source/destination
    ip.ttl                  → IP Time to Live
    tcp.srcport, tcp.dstport
    tcp.flags.syn, tcp.flags.ack, tcp.flags.reset
    tcp.window_size         → TCP window size
    dns.qry.name            → DNS query name
    http.host               → HTTP Host header
    http.request.method     → GET, POST, etc.
    tls.handshake.extensions_server_name  → TLS SNI
    frame.len               → total frame size
    frame.time              → absolute timestamp

  Operators:
    ==  (equal)            !=  (not equal)
    >   (greater than)     <   (less than)
    >=  >=  (greater/less or equal)
    contains  (string contains — case sensitive)
    matches   (Perl-compatible regex)
    in        (membership — value in {set})

  Combining:
    and  (both must match)
    or   (either matches)
    not  (negate)
    ( )  grouping

  Existence test (field exists in packet):
    tcp.options.timestamp   → packet has TCP timestamp option
    http.request            → packet has HTTP request

  Slice operator (byte range):
    tcp.payload[0:4]        → first 4 bytes of TCP payload
    tcp.payload[0] == 0x16  → first byte equals 0x16 (TLS handshake)

Essential Forensics Display Filters

Textforensics-display-filters.txt
── CONNECTION ANALYSIS ──────────────────────────────────────────────
All SYN packets (connection attempts):
  tcp.flags.syn == 1 and tcp.flags.ack == 0

All RST packets (connection resets):
  tcp.flags.reset == 1

Failed connections (SYN with no SYN-ACK — port closed/filtered):
  tcp.flags.syn == 1 and tcp.flags.ack == 0
  (then look for absence of matching SYN-ACK in same stream)

Long connections (potential C2 keep-alive):
  tcp.time_relative > 3600

── DNS ──────────────────────────────────────────────────────────────
All DNS queries:
  dns.flags.response == 0

DNS queries for specific domain:
  dns.qry.name contains "evil.com"

DNS queries with NX response (NXDOMAIN — DGA candidates):
  dns.flags.rcode == 3

DNS TXT record queries (possible tunneling):
  dns.qry.type == 16

── HTTP ─────────────────────────────────────────────────────────────
All HTTP POST requests:
  http.request.method == "POST"

HTTP with large response body:
  http.content_length > 1000000

HTTP to non-standard ports (port 80 is standard — port 8080 is not):
  http and not tcp.port == 80

Specific HTTP user-agent:
  http.user_agent contains "PowerShell"
  http.user_agent contains "python-requests"
  http.user_agent == "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1)"

── TLS ──────────────────────────────────────────────────────────────
All TLS ClientHello (new TLS sessions):
  tls.handshake.type == 1

TLS connections to specific destination:
  tls and ip.dst == 185.220.101.47

TLS with specific SNI:
  tls.handshake.extensions_server_name == "updates.microsoft.com"

── CREDENTIAL THEFT ─────────────────────────────────────────────────
NTLM authentication in SMB:
  ntlmssp

Kerberos TGS requests (Kerberoasting: many in rapid succession):
  kerberos.msg_type == 12

LDAP bind requests:
  ldap.protocolOp == 0

── SCANNING ─────────────────────────────────────────────────────────
Port scan indicator (many SYNs from one source):
  ip.src == 10.0.1.50 and tcp.flags.syn == 1 and tcp.flags.ack == 0

ICMP tunnel candidate (large ICMP payload):
  icmp and frame.len > 100

── EXFILTRATION ─────────────────────────────────────────────────────
Large outbound transfers:
  ip.dst != 10.0.0.0/8 and frame.len > 1400

DNS exfiltration candidate (long DNS labels):
  dns.qry.name matches "[a-z0-9]{30,}"

Regular Expressions in Display Filters

Textregex-display-filters.txt
The "matches" operator uses PCRE (Perl-Compatible Regular Expressions).
Use it when "contains" is too loose or you need pattern matching.

High-entropy DNS domain (potential DGA):
  dns.qry.name matches "[a-z0-9]{15,}\.(com|net|org|io)$"

Base64 content in HTTP URI (data encoded in URL):
  http.request.uri matches "[A-Za-z0-9+/]{40,}={0,2}"

PowerShell encoded command in HTTP:
  http contains "powershell" and http matches "-[Ee][Nn][Cc]"

IPv4 address in DNS query (reverse lookup abuse or IP-as-domain):
  dns.qry.name matches "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}"

Cobalt Strike default URI patterns:
  http.request.uri matches "^/(jquery-|updates|load|push|poll)"

TLS certificate Common Name anomaly (IP address as CN):
  tls.handshake.certificate matches "[0-9]{1,3}\.[0-9]{1,3}"

IMPORTANT: "matches" is slow on large captures.
Prefer "contains" for simple substring search.
Use "matches" only when you need PCRE power (anchors, groups, quantifiers).

Membership Operator and Filter Chaining

Textadvanced-filters.txt
── IN OPERATOR (membership test) ────────────────────────────────────
Matches if value is in the given set:

  tcp.dstport in {80, 443, 8080, 8443}    → common HTTP/HTTPS ports
  ip.dst in {185.220.101.47, 45.33.32.156}  → multiple suspect IPs
  dns.qry.type in {1, 28, 16, 33}          → A, AAAA, TXT, SRV records

── FILTER CHAINING ──────────────────────────────────────────────────
Find all HTTP POST to any of several suspicious IPs:
  http.request.method == "POST" and ip.dst in {1.2.3.4, 5.6.7.8}

Find DNS queries that are also large (tunneling):
  dns and frame.len > 200 and dns.flags.response == 0

Find TLS connections with short validity certs (< 1 day):
  tls.handshake.certificate  (then check tls.handshake.cert_expiration)

NXDOMAIN responses for high-entropy names (DGA):
  dns.flags.rcode == 3 and dns.qry.name matches "[a-z0-9]{12,}"

── SAVING NAMED FILTERS ─────────────────────────────────────────────
Save frequently used filters:
  1. Type the filter in the display filter bar
  2. Click the bookmark icon (left of filter bar) → Save this filter
  3. Give it a descriptive name: "C2 Beaconing Candidates"
  4. Access via bookmark icon dropdown — appears in every session

Named filters work within a Wireshark Profile, so combine
with a custom column layout and coloring rules into one
"Forensics" profile you load at the start of every investigation.
Why learning filter syntax pays off more than memorizing specific filters

Every attack is slightly different. Cobalt Strike with a custom profile uses different URI paths than the defaults. An attacker using PowerShell WebClient sends a specific User-Agent string you've never seen before. If you only know memorized filters for known patterns, you'll miss novel variations. If you understand the filter syntax — how to access any protocol field, how to combine conditions, how to write a regex — you can build a custom filter for any observable characteristic in the packet, including fields you've never searched before. The investment: spend 30 minutes reading Wireshark's display filter documentation once, then you can construct any filter from first principles.

Q & A

Q: My display filter returns zero results but I can see the packets I want visually in the list. What's wrong?

The most common cause is a field name error — Wireshark silently treats an unknown field as "never present" rather than throwing an error. The filter bar stays green (valid syntax) but never matches because the field doesn't exist. To debug: (1) Click on a packet that should match. In the Details pane, find the field you want to filter on. Right-click it and select "Apply as Filter → Selected." This auto-generates the correct filter expression using the exact field name Wireshark knows. (2) Check that the protocol is being decoded as you expect. If packets on port 4444 are decoded as "TCP" (not HTTP), then http.host will never match because Wireshark doesn't know it's HTTP. Right-click the port in a packet → "Decode As" → HTTP to force the dissector. (3) Verify operator matching: contains is case-sensitive. ip.dst == 10.0.0.0/8 doesn't work (use ip.dst matches "10\\..*" or ip.dst[0] == 10 for subnet checks).