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.
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
#!/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
#!/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}")
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.