Chapter 24

HTTP/HTTPS Analysis

HTTP is the most common transport for both legitimate traffic and attacker activity. Attackers use it because it blends in, crosses almost every firewall, and is trivially proxied. Understanding how to extract forensic evidence from HTTP — request headers, URIs, user agents, response codes, content types, and the payload — is foundational to network forensics. HTTPS adds TLS but doesn't eliminate metadata; the SNI, certificate, and connection timing remain visible.

Scenario

An analyst has a PCAP from a suspected web shell compromise. The attacker connected to an internal web server on port 80 and issued commands via POST requests to /cms/upload.php. The request bodies contain base64-encoded PowerShell. The responses contain the command output. You need to: identify every POST to that URI, decode each request body, extract the commands executed, and map the timeline.

HTTP Evidence Hierarchy

  HTTP Forensic Evidence — What Each Layer Tells You
  ═══════════════════════════════════════════════════════════════════

  Request Line:  GET/POST/PUT  +  URI path + query string
    └── URI reveals resource accessed; query string may contain commands

  Request Headers:
    ├── Host: actual target hostname (useful for virtual hosting)
    ├── User-Agent: client identity (often fake in C2 but still distinctive)
    ├── Referer: navigation path (legitimate browsers always include it)
    ├── Cookie: session tokens (credential theft, session hijacking)
    ├── Authorization: Basic/Bearer credentials
    ├── Content-Type: tells you how to parse the body
    └── X-Forwarded-For: true client IP behind proxies

  Request Body (POST/PUT):
    └── May contain: commands (web shells), credentials (phishing),
        exfiltrated data (upload), binary payloads (file uploads)

  Response Line: HTTP status code
    ├── 200 OK: command executed successfully (web shell)
    ├── 302 Redirect: phishing flow
    ├── 403/404: scanning — attacker mapping server paths
    └── 500 Internal Server Error: exploitation attempt caused crash

  Response Headers:
    ├── Server: server software version (fingerprinting)
    ├── Set-Cookie: session cookie creation
    └── Content-Type: how to parse the response body

  Response Body:
    └── Command output (web shells), stolen data (exfil),
        malware downloads (malware delivery)

HTTP Analysis with tshark

bashhttp-analysis.sh
#!/bin/bash
PCAP="$1"

echo "=== HTTP Request Summary ==="
tshark -r "$PCAP" -n -Y "http.request" -T fields \
  -E separator="|" \
  -e frame.time_epoch \
  -e ip.src \
  -e ip.dst \
  -e http.request.method \
  -e http.host \
  -e http.request.uri \
  -e http.user_agent \
  | sort | uniq -c | sort -rn | head -50

echo ""
echo "=== POST requests with bodies ==="
tshark -r "$PCAP" -n \
  -Y "http.request.method == POST" \
  -T fields \
  -E separator="\t" \
  -e frame.time_epoch \
  -e ip.src \
  -e http.host \
  -e http.request.uri \
  -e http.file_data \
  | head -100

echo ""
echo "=== HTTP responses with error codes ==="
tshark -r "$PCAP" -n \
  -Y "http.response.code >= 400" \
  -T fields \
  -E separator="\t" \
  -e frame.time_epoch \
  -e ip.dst \
  -e http.request.uri \
  -e http.response.code \
  | sort | uniq -c | sort -rn | head -50

echo ""
echo "=== Unique User Agents ==="
tshark -r "$PCAP" -n -Y "http.user_agent" -T fields \
  -e http.user_agent \
  | sort | uniq -c | sort -rn

echo ""
echo "=== Non-standard HTTP ports ==="
tshark -r "$PCAP" -n -Y "http.request and not (tcp.port == 80 or tcp.port == 8080 or tcp.port == 8443 or tcp.port == 443)" \
  -T fields -e ip.dst -e tcp.dstport -e http.host

Web Shell Traffic Patterns

IndicatorLegitimateWeb Shell
HTTP methodGET dominant, POST for formsPOST for commands, GET for check-in
URI patternStable paths matching site structureUnusual PHP/ASPX in non-application dirs
Request body encodingForm data, JSON, XMLBase64 blobs, concatenated chars, gzip+base64
User-AgentReal browser string with versioncurl, python-requests, custom, or empty
Response bodyHTML page contentCommand output: plaintext, directory listings, file contents
Session patternMultiple requests per session, navigationIndividual commands: one request → one response
TimingHuman-paced click patternsSub-second consistent intervals (scripted)
Response sizeVaries with page contentConsistent small responses (command output)
Pythonweb-shell-hunt.py
#!/usr/bin/env python3
"""Hunt for web shell activity in HTTP traffic."""
import subprocess, base64, re, sys

PCAP = sys.argv[1]

def tshark_fields(pcap, display_filter, fields):
    cmd = ["tshark", "-r", pcap, "-n", "-Y", display_filter, "-T", "fields",
           "-E", "separator=\t"] + [f for fld in fields for f in ["-e", fld]]
    result = subprocess.run(cmd, capture_output=True, text=True)
    rows = []
    for line in result.stdout.splitlines():
        parts = line.split("\t")
        rows.append(parts)
    return rows

# Find POST requests with potential command injection patterns
print("=== POST requests with encoded payloads ===")
for row in tshark_fields(PCAP, "http.request.method==POST",
                          ["frame.time_epoch","ip.src","http.host","http.request.uri","http.file_data"]):
    if len(row) < 5:
        continue
    ts, src, host, uri, body = row[0], row[1], row[2], row[3], row[4]
    # Check for base64 in body
    if re.search(r'[A-Za-z0-9+/]{40,}={0,2}', body):
        # Try to decode
        matches = re.findall(r'[A-Za-z0-9+/]{40,}={0,2}', body)
        for m in matches[:3]:
            try:
                decoded = base64.b64decode(m).decode("utf-8", errors="replace")
                if any(kw in decoded.lower() for kw in ["powershell","cmd","whoami","net user","tasklist"]):
                    print(f"  [{ts}] {src} POST {host}{uri}")
                    print(f"  DECODED: {decoded[:200]}")
            except Exception:
                pass

# Find unusual file extensions being requested (web shell paths)
SUSPICIOUS_EXT = [".php",".aspx",".jsp",".cfm",".ashx",".shtml"]
print("\n=== Requests to script files ==="  )
seen_paths = set()
for row in tshark_fields(PCAP, "http.request",
                          ["ip.src","http.host","http.request.uri","http.request.method"]):
    if len(row) < 4:
        continue
    src, host, uri, method = row[0], row[1], row[2], row[3]
    if any(uri.lower().endswith(ext) for ext in SUSPICIOUS_EXT):
        key = f"{host}{uri}"
        if key not in seen_paths:
            seen_paths.add(key)
            print(f"  [{method}] {src} → {host}{uri}")
Why HTTP User-Agent is both useful and unreliable

User-Agent is the single most commonly faked HTTP header by both attackers and legitimate applications. C2 frameworks routinely spoof User-Agent to mimic Chrome or Firefox. However, User-Agent is still forensically valuable for two reasons: (1) Outliers are meaningful: in an environment where 99% of HTTP traffic uses Chrome or Firefox User-Agents, a single connection using python-requests/2.28.0 or a Cobalt Strike default profile UA stands out immediately. (2) Consistency is a fingerprint: if a host sends 100 HTTPS connections all with the exact same User-Agent string including micro-version (e.g., Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36) but never updates, that suggests automation. Real Chrome updates frequently. A static User-Agent across months of traffic from the same host is a weak but real indicator.

Q & A

Q: The web shell POSTs have Content-Type: application/x-www-form-urlencoded but the body is just random-looking base64. How does that work forensically?

The Content-Type header claims the body is form-encoded (key=value pairs), but the attacker's web shell handler on the server decodes base64 regardless of Content-Type — the server-side PHP doesn't check the declared content type, it just reads the raw POST body and base64-decodes it. For forensics: don't rely on Content-Type to tell you how to parse HTTP bodies. Instead: (1) Look at the raw body directly, (2) Check for base64 patterns with regex [A-Za-z0-9+/]{20,}={0,2}, (3) Try to decode any sufficiently long base64 string you find, (4) Look for PowerShell, cmd, or shell command keywords in the decoded output. Also: web shells often obfuscate further — base64 inside URL encoding inside another base64 layer. Decoding one layer may reveal another base64 string. Try decoding up to 3 layers deep before giving up.