Chapter 28

SMTP and Email Forensics

SMTP network forensics covers two scenarios: (1) analyzing cleartext SMTP traffic to reconstruct emails, extract attachments, and identify phishing campaigns or data exfiltration via email, (2) investigating mail server logs and headers to trace an email's path through the internet. Email remains one of the primary initial access vectors — phishing attachments, links, and business email compromise (BEC) all start with an email landing in an inbox.

Scenario

An employee received a phishing email that delivered a malicious macro document. Your mail gateway logged the SMTP session. You need to: (1) extract the full email with attachment from PCAP, (2) reconstruct the email headers to identify the sending infrastructure, (3) extract the macro-bearing Office document, (4) identify the C2 domain embedded in the macro, and (5) determine if the same sender targeted other employees. All of this is possible from SMTP/network forensics before touching endpoint forensics.

SMTP Protocol Forensics

  SMTP Session — Key Forensic Points
  ═══════════════════════════════════════════════════════════════════

  TCP Connection: attacker_IP:ephemeral → mail_server:25 or :587

  EHLO/HELO: sender's claimed hostname
    └── May be forged but still recorded; compare to connecting IP

  MAIL FROM: envelope sender (Return-Path)
    └── Different from From: header — this is where bounces go

  RCPT TO: recipient address(es)
    └── Multiple RCPT = mass phishing attempt from one connection

  DATA: begin of email content
    └── Everything after DATA until lone "." is the email message:
          Headers: From, To, Subject, Date, X-Mailer, Received, MIME
          Body: text/plain, text/html
          Attachments: base64 encoded MIME parts

  Key forensic artifacts:
  ├── Received: headers (trace email's path through MTAs)
  ├── X-Originating-IP: true sender IP (added by some servers)
  ├── MIME-Version, Content-Type, Content-Transfer-Encoding
  ├── MIME boundary: separates body from attachments
  └── Base64-encoded attachment data (decode → get attachment bytes)

SMTP Session Reconstruction

bashsmtp-extract.sh
#!/bin/bash
PCAP="$1"
OUT_DIR="${2:-smtp_output}"
mkdir -p "$OUT_DIR"

echo "=== SMTP connections ==="
tshark -r "$PCAP" -n -Y "smtp" -T fields \
  -E separator="\t" \
  -e ip.src \
  -e ip.dst \
  -e tcp.dstport \
  | sort | uniq -c | sort -rn | head -20

echo ""
echo "=== SMTP commands (MAIL FROM, RCPT TO, EHLO) ==="
tshark -r "$PCAP" -n -Y "smtp.req" -T fields \
  -E separator="\t" \
  -e frame.time_epoch -e ip.src -e smtp.req.command -e smtp.req.parameter \
  | head -50

echo ""
echo "=== Email subjects from DATA section ==="
tshark -r "$PCAP" -n -Y "smtp.data.fragment" -T fields \
  -e smtp.data.fragment \
  | grep -i "^Subject:" | head -30

# Export SMTP objects using Wireshark's --export-objects
echo ""
echo "=== Exporting email objects ==="
tshark -r "$PCAP" --export-objects "imf,$OUT_DIR" 2>/dev/null || true
ls -la "$OUT_DIR"/ 2>/dev/null | head -20

echo ""
echo "=== Extract email from SMTP DATA stream ==="
tshark -r "$PCAP" -n -Y "smtp" -w /tmp/smtp_only.pcap 2>/dev/null
# Follow TCP stream for each SMTP connection
tshark -r /tmp/smtp_only.pcap -n -z "follow,tcp,ascii,0" 2>/dev/null \
  | head -200

Email Header Analysis — Tracing Source

Pythonemail-header-trace.py
#!/usr/bin/env python3
"""
Parse email headers to trace delivery path and identify phishing indicators.
Input: raw email file (.eml) or email extracted from PCAP via tshark
"""
import email, sys, re
from email import policy
from email.parser import BytesParser
from datetime import datetime

def parse_received_headers(msg) -> list:
    """Extract hop chain from Received: headers."""
    received = msg.get_all("Received", [])
    hops = []
    for r in received:
        # Extract IP from Received header
        ips = re.findall(r'\[(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\]', r)
        # Extract hostname
        by_match = re.search(r'by (\S+)', r)
        from_match = re.search(r'from (\S+)', r)
        date_match = re.search(r';(.+)$', r)
        hops.append({
            "from": from_match.group(1) if from_match else "",
            "by": by_match.group(1) if by_match else "",
            "ips": ips,
            "date": date_match.group(1).strip() if date_match else "",
        })
    return hops

def check_phishing_indicators(msg) -> list:
    """Check for common phishing indicators in headers."""
    indicators = []
    sender_domain = re.search(r'@([\w.-]+)', msg.get("From", ""))
    reply_to_domain = re.search(r'@([\w.-]+)', msg.get("Reply-To", ""))

    # Reply-To different from From domain (common phishing)
    if sender_domain and reply_to_domain:
        if sender_domain.group(1).lower() != reply_to_domain.group(1).lower():
            indicators.append(f"Reply-To domain differs from From domain: "
                            f"{sender_domain.group(1)} vs {reply_to_domain.group(1)}")

    # Suspicious X-Mailer
    mailer = msg.get("X-Mailer", "")
    if any(kw in mailer.lower() for kw in ["python", "curl", "php", "perl"]):
        indicators.append(f"Suspicious X-Mailer: {mailer}")

    # No DKIM signature
    if not msg.get("DKIM-Signature"):
        indicators.append("No DKIM signature (unauthenticated)")

    # Authentication results
    auth_results = msg.get("Authentication-Results", "")
    if "dmarc=fail" in auth_results.lower():
        indicators.append(f"DMARC: FAIL — domain spoofing detected")
    if "spf=fail" in auth_results.lower():
        indicators.append(f"SPF: FAIL — unauthorized sender IP")

    return indicators

# Parse email
with open(sys.argv[1], "rb") as f:
    msg = BytesParser(policy=policy.default).parse(f)

print(f"From: {msg['From']}")
print(f"To: {msg['To']}")
print(f"Subject: {msg['Subject']}")
print(f"Date: {msg['Date']}")
print(f"Reply-To: {msg.get('Reply-To', 'not set')}")
print(f"X-Originating-IP: {msg.get('X-Originating-IP', 'not set')}")
print(f"X-Mailer: {msg.get('X-Mailer', 'not set')}")
print(f"Message-ID: {msg.get('Message-ID', 'not set')}")

print("\n=== Delivery Hop Chain (most recent first) ===")
for i, hop in enumerate(parse_received_headers(msg)):
    print(f"Hop {i+1}: {hop['from']} → {hop['by']}")
    if hop['ips']:
        print(f"         IPs: {hop['ips']}")

print("\n=== Phishing Indicators ===")
for indicator in check_phishing_indicators(msg):
    print(f"  [!] {indicator}")
if not check_phishing_indicators(msg):
    print("  No obvious header-based indicators")

print("\n=== Attachments ===")
for part in msg.walk():
    if part.get_content_maintype() == 'multipart':
        continue
    filename = part.get_filename()
    content_type = part.get_content_type()
    if filename or part.get_content_disposition() == 'attachment':
        payload = part.get_payload(decode=True)
        size = len(payload) if payload else 0
        print(f"  {filename or '(no name)'}  type={content_type}  size={size} bytes")
        if filename:
            safe_name = re.sub(r'[^\w.-]', '_', filename)
            with open(f"attachment_{safe_name}", "wb") as f:
                f.write(payload or b"")
            print(f"  Saved to: attachment_{safe_name}")

Q & A

Q: The phishing email passed DMARC and SPF checks. How is that possible?

Passing DMARC and SPF doesn't mean the email is legitimate — it means the attacker played by the rules of the domain they're using. Common scenarios: (1) Lookalike domain: attacker registers microsof-t.com (note hyphen), sets up valid SPF/DKIM/DMARC for it, and sends email from that domain. All auth checks pass because the domain is correctly configured — it's just not microsoft.com. (2) Compromised legitimate account: attacker uses a legitimate Google Workspace or Office 365 account. All auth passes because the sender is genuinely authorized to send from that domain. (3) Sub-domain attacks: if the parent domain has DMARC p=none, sub-domain spoofing may pass. (4) Indirect phishing: use a legitimate marketing email platform (MailChimp, Sendgrid) to send phishing. The platform's DKIM signs the message. Detection for these cases: look at the visual display domain vs the technical sender, check the Reply-To, examine the body for suspicious URLs, and look at the IP reputation of the sending MTA — even if auth passes, the first hop IP may be on a blocklist.