Chapter 15

tshark for IOC Extraction

Every investigation produces a list of Indicators of Compromise: IP addresses, domains, URLs, JA3 hashes, certificate fingerprints, user-agent strings. tshark extracts all of these from a PCAP in one pipeline. This chapter builds a complete IOC extraction script that outputs a structured IOC list ready for threat intel enrichment and SIEM ingestion.

Scenario

You've confirmed an incident. Management wants a complete IOC list within the hour — every IP, domain, URL, TLS certificate, and user-agent string that appeared in the attack traffic, with no legitimate infrastructure included. You have a 2 GB PCAP of the attack window. The tshark pipeline extracts, deduplicates, and formats all IOCs in 8 minutes, then enriches them against VirusTotal in another 10 minutes.

Complete IOC Extraction Pipeline

Bashioc-extraction.sh
#!/bin/bash
# Complete IOC extraction from a PCAP
# Usage: ./ioc-extraction.sh capture.pcap /output/dir/

PCAP="$1"
OUT="$2"
mkdir -p "$OUT"

echo "Extracting IOCs from: $PCAP"
echo "Output directory: $OUT"

# ── DESTINATION IPs (external only) ──────────────────────────────────
echo "[1/7] Extracting external destination IPs..."
tshark -r "$PCAP" -n -q \
    -Y "not ip.dst in {10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8}" \
    -T fields -e ip.dst 2>/dev/null | \
    sort -u > "$OUT/ips.txt"
echo "  → $(wc -l < $OUT/ips.txt) unique external IPs"

# ── DNS QUERY NAMES ──────────────────────────────────────────────────
echo "[2/7] Extracting DNS query names..."
tshark -r "$PCAP" -n -q \
    -Y "dns.flags.response==0 and dns.qry.name" \
    -T fields -e dns.qry.name 2>/dev/null | \
    grep -v "^$" | sort -u > "$OUT/dns_queries.txt"
echo "  → $(wc -l < $OUT/dns_queries.txt) unique DNS names"

# ── HTTP HOST HEADERS ─────────────────────────────────────────────────
echo "[3/7] Extracting HTTP hosts..."
tshark -r "$PCAP" -n -q \
    -Y "http.host" \
    -T fields -e http.host 2>/dev/null | \
    grep -v "^$" | sort -u > "$OUT/http_hosts.txt"
echo "  → $(wc -l < $OUT/http_hosts.txt) unique HTTP hosts"

# ── HTTP URLs (host + URI) ────────────────────────────────────────────
echo "[4/7] Extracting HTTP URLs..."
tshark -r "$PCAP" -n -q \
    -Y "http.request" \
    -T fields -e http.host -e http.request.uri 2>/dev/null | \
    awk '{if ($1 && $2) print "http://" $1 $2}' | \
    sort -u > "$OUT/http_urls.txt"
echo "  → $(wc -l < $OUT/http_urls.txt) unique HTTP URLs"

# ── TLS SNI ───────────────────────────────────────────────────────────
echo "[5/7] Extracting TLS SNI values..."
tshark -r "$PCAP" -n -q \
    -Y "tls.handshake.extensions_server_name" \
    -T fields -e tls.handshake.extensions_server_name 2>/dev/null | \
    grep -v "^$" | sort -u > "$OUT/tls_sni.txt"
echo "  → $(wc -l < $OUT/tls_sni.txt) unique TLS SNI names"

# ── HTTP USER AGENTS ──────────────────────────────────────────────────
echo "[6/7] Extracting User-Agent strings..."
tshark -r "$PCAP" -n -q \
    -Y "http.user_agent" \
    -T fields -e http.user_agent 2>/dev/null | \
    grep -v "^$" | sort -u > "$OUT/user_agents.txt"
echo "  → $(wc -l < $OUT/user_agents.txt) unique User-Agent strings"

# ── TLS CERTIFICATE SUBJECTS ──────────────────────────────────────────
echo "[7/7] Extracting TLS certificate subjects..."
tshark -r "$PCAP" -n -q \
    -Y "tls.handshake.certificate" \
    -T fields \
    -e tls.handshake.certificate \
    -e x509sat.printableString \
    -e x509sat.uTF8String 2>/dev/null | \
    grep -v "^$" | sort -u > "$OUT/tls_certs.txt"
echo "  → Certificate data extracted"

echo ""
echo "=== IOC Summary ==="
echo "External IPs:    $(wc -l < $OUT/ips.txt)"
echo "DNS queries:     $(wc -l < $OUT/dns_queries.txt)"
echo "HTTP hosts:      $(wc -l < $OUT/http_hosts.txt)"
echo "HTTP URLs:       $(wc -l < $OUT/http_urls.txt)"
echo "TLS SNI:         $(wc -l < $OUT/tls_sni.txt)"
echo "User-Agents:     $(wc -l < $OUT/user_agents.txt)"

VirusTotal Enrichment Pipeline

Pythonvt-enrich-iocs.py
#!/usr/bin/env python3
"""
Enrich extracted IOCs against VirusTotal.
Usage: python3 vt-enrich-iocs.py  
Outputs: enriched_iocs.json with detection counts per IOC
"""
import json, time, sys
from pathlib import Path
import urllib.request
import urllib.parse

IOC_DIR = Path(sys.argv[1])
VT_API_KEY = sys.argv[2]
VT_URL = "https://www.virustotal.com/api/v3"

def vt_lookup(resource_type: str, resource: str) -> dict:
    url = f"{VT_URL}/{resource_type}/{urllib.parse.quote(resource, safe='')}"
    req = urllib.request.Request(url, headers={"x-apikey": VT_API_KEY})
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            data = json.loads(resp.read())
            stats = data.get("data", {}).get("attributes", {}).get("last_analysis_stats", {})
            return {
                "malicious": stats.get("malicious", 0),
                "suspicious": stats.get("suspicious", 0),
                "harmless": stats.get("harmless", 0),
                "undetected": stats.get("undetected", 0)
            }
    except Exception as e:
        return {"error": str(e)}

results = []

# Enrich IPs
for ip in (IOC_DIR / "ips.txt").read_text().splitlines():
    ip = ip.strip()
    if not ip:
        continue
    print(f"Checking IP: {ip}")
    result = vt_lookup("ip_addresses", ip)
    results.append({"type": "ip", "value": ip, "vt": result})
    time.sleep(0.25)  # VT free tier: 4 lookups/sec max

# Enrich domains
for domain in (IOC_DIR / "dns_queries.txt").read_text().splitlines():
    domain = domain.strip()
    if not domain:
        continue
    print(f"Checking domain: {domain}")
    result = vt_lookup("domains", domain)
    results.append({"type": "domain", "value": domain, "vt": result})
    time.sleep(0.25)

# Write results
output = IOC_DIR / "enriched_iocs.json"
with open(output, "w") as f:
    json.dump(results, f, indent=2)

# Print high-confidence malicious IOCs
print("\n=== Malicious IOCs (≥3 detections) ===")
for r in results:
    if r["vt"].get("malicious", 0) >= 3:
        print(f"  {r['type']:8} | {r['value']:50} | {r['vt']['malicious']} malicious")

print(f"\nFull results: {output}")
Common mistake: including internal/legitimate IPs in the IOC list

The filter not ip.dst in {10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8} removes RFC 1918 private addresses, but several categories remain that are not attack infrastructure: (1) CDN IPs (Cloudflare, Akamai, Fastly) — legitimate traffic from browsers and update clients hits these constantly, (2) Microsoft/Google update server IPs — Windows Update, Chrome, Office 365, (3) Your public IP range — if the capture includes packets from internal hosts to your own public web services. Always filter your IOC list against a known-good allowlist (your CDN/cloud vendor IP ranges, Microsoft Azure/O365 IPs, Google IPs). The raw tshark output contains all external IPs including legitimate ones — manual triage or allowlist filtering is required before the list is actionable.

Deduplication and Sorting

Bashioc-dedup.sh