Chapter 2.4 - Encrypted Traffic Analysis & TLS Inspection
Module 2: Traffic Analysis & Intrusion Detection Prerequisites: Chapter 2.1 (Packet Analysis), 2.2 (IDS/IPS), 2.3 (Network Forensics)
Table of Contents
- Why Encrypted Traffic Analysis Matters
- TLS Internals - Handshake, Record Layer & Key Material
- Passive Fingerprinting - JA3, JA3S, JARM & HASSH
- Encrypted Traffic Analysis Without Decryption
- TLS Interception - MITM Proxies & SSL Inspection
- Decryption with Pre-Master Secrets & SSLKEYLOGFILE
- Certificate Intelligence & PKI Abuse
- Tools Walkthrough - Zeek, Wireshark, mitmproxy, Suricata
- Evasion - Domain Fronting, ESNI/ECH & Certificate Mimicry
- MITRE ATT&CK Mapping
1. Why Encrypted Traffic Analysis Matters
Encryption is the single biggest obstacle to network-layer visibility. As of 2024, over 95% of web traffic is TLS-protected, and the same protocols used to secure banking sessions are used by malware to exfiltrate data, receive C2 instructions, and tunnel arbitrary protocols. The security team that treats all encrypted traffic as a black box has forfeited visibility over the majority of their network.
The response is not binary - full decryption is not always feasible or legal, and it introduces its own attack surface. The discipline of Encrypted Traffic Analysis (ETA) covers a spectrum:
- Passive fingerprinting - identify client/server software, detect anomalies in TLS parameters without touching plaintext
- Metadata analysis - flow duration, byte patterns, packet timing, certificate fields - all available without decryption
- Selective decryption - intercept specific traffic categories (outbound corporate browsing) at a controlled choke point
- Key log decryption - use session keys exported from the endpoint to decrypt captures in Wireshark or Zeek post-hoc
- Certificate intelligence - identify malicious infrastructure by certificate attributes, CA anomalies, or self-signed certs on unexpected ports
Each technique has a different legal, operational, and architectural profile. This chapter covers all of them.
2. TLS Internals - Handshake, Record Layer & Key Material
Understanding what TLS exposes in the clear - and what it doesn't - is the foundation of ETA. Depending on the TLS version and configuration, a substantial amount of metadata remains visible even on encrypted sessions.
TLS 1.2 Handshake (Partially Visible)
In TLS 1.2, the ClientHello and ServerHello are sent in the clear. The Certificate message is also unencrypted, exposing the server's certificate chain. Only the Finished and Application Data records are encrypted.
Client Server
| |
|--- ClientHello (plaintext) ---------------->| # Cipher suites, extensions, SNI, random
|<-- ServerHello (plaintext) -----------------| # Chosen cipher, session ID, random
|<-- Certificate (plaintext) -----------------| # Server cert chain - FULLY VISIBLE
|<-- ServerHelloDone (plaintext) -------------|
|--- ClientKeyExchange (plaintext) ---------->| # RSA-encrypted premaster OR ECDH key share
|--- ChangeCipherSpec (plaintext) ----------->|
|--- Finished (encrypted) ------------------->|
|<-- ChangeCipherSpec (plaintext) ------------|
|<-- Finished (encrypted) --------------------|
|<=== Application Data (encrypted) ==========>|
TLS 1.3 Handshake (More Encrypted)
TLS 1.3 reduces round trips and encrypts significantly more of the handshake. The Certificate message is encrypted. However, SNI is still sent in plaintext in the ClientHello unless ECH (Encrypted Client Hello) is used - covered in Section 9.
Client Server
| |
|--- ClientHello (plaintext) ---------------->| # key_share, SNI (plaintext!), supported_versions
|<-- ServerHello (plaintext) -----------------| # key_share response
|<-- {EncryptedExtensions} (encrypted) -------| # ALPN, etc.
|<-- {Certificate} (encrypted) ---------------| # No longer visible passively
|<-- {CertificateVerify} (encrypted) ---------|
|<-- {Finished} (encrypted) -----------------|
|--- {Finished} (encrypted) ----------------->|
|<=== Application Data (encrypted) ==========>|
What Remains Visible at the Wire (Both TLS Versions)
| Field | TLS 1.2 | TLS 1.3 | Notes |
|---|---|---|---|
| SNI (Server Name Indication) | Plaintext | Plaintext | Unless ECH is used |
| Server Certificate | Plaintext | Encrypted | Major visibility regression in 1.3 |
| Client Certificate | Plaintext | Encrypted | mTLS client identity hidden in 1.3 |
| Cipher Suite List | Plaintext | Plaintext | Fingerprinting source |
| TLS Extensions | Plaintext | Plaintext | Key source for JA3 |
| ALPN (protocol negotiated) | Plaintext | Partial | h2, http/1.1, etc. |
| Packet sizes & timing | Always | Always | ML feature |
| Flow duration & bytes | Always | Always | Behavioral analytics |
Inspecting TLS with OpenSSL
# Full TLS handshake inspection - see certificate, cipher, protocol version
openssl s_client \
-connect target.example.com:443 \ # Host:port to connect
-servername target.example.com \ # SNI value to send (can differ from connect target)
-showcerts \ # Print full certificate chain
-tlsextdebug \ # Show all TLS extensions raw
-msg 2>&1 | head -100 # Show raw protocol messages
# Force specific TLS version to test server support
openssl s_client -connect target.example.com:443 \
-tls1_2 \ # Force TLS 1.2 only
-cipher 'ECDHE-RSA-AES256-GCM-SHA384' # Force specific cipher
# Extract certificate details only
openssl s_client -connect target.example.com:443 \
-servername target.example.com 2>/dev/null | \
openssl x509 -noout -text # Parse the certificate
# Check certificate validity dates
openssl s_client -connect target.example.com:443 2>/dev/null | \
openssl x509 -noout -dates
# Check certificate Subject Alternative Names (SANs) - critical for domain fronting detection
openssl s_client -connect target.example.com:443 2>/dev/null | \
openssl x509 -noout -ext subjectAltName
# Test if server accepts client certificates (mTLS)
openssl s_client -connect api.internal:8443 \
-cert /path/to/client.crt \
-key /path/to/client.key \
-CAfile /path/to/ca-chain.pem \
-verify_return_error
3. Passive Fingerprinting - JA3, JA3S, JARM & HASSH
JA3 - Client TLS Fingerprint
JA3 computes an MD5 hash from five fields extracted from the TLS ClientHello:
JA3 = MD5( SSLVersion, Ciphers, Extensions, EllipticCurves, EllipticCurvePointFormats )
These fields are determined by the TLS library used by the application, not by the application logic. A Cobalt Strike beacon compiled on Linux using its default OpenSSL build will produce the same JA3 regardless of what domain it's connecting to. This makes JA3 a reliable fingerprint of the client software - browser, curl, Python requests, malware framework.
# Extract JA3 hashes from a PCAP using tshark
tshark -r capture.pcap \
-Y 'tls.handshake.type == 1' \ # Filter: ClientHello only
-T fields \
-e ip.src \ # Source IP
-e tls.handshake.ja3 \ # JA3 hash
-e tls.handshake.extensions_server_name \ # SNI
-E header=y -E separator=,
# Using ja3 Python tool against live interface
pip install pyja3
python3 -m pyja3 -i eth0 # Live capture
python3 -m pyja3 -r /captures/malware.pcap # PCAP mode
# zeek will compute JA3 natively - check ssl.log
zeek -r capture.pcap -C
cat ssl.log | zeek-cut ts id.orig_h id.resp_h ja3 ja3s server_name | head -20
Known malicious JA3 hashes (partial list for illustration):
| JA3 Hash | Associated Tool |
|---|---|
72a589da586844d7f0818ce684948eea | Cobalt Strike default beacon |
a0e9f5d64349fb13191bc781f81f42e1 | Metasploit Meterpreter |
e7d705a3286e19ea42f587b07571b559 | Dridex malware |
7dd80081a2a0b18aeed51b2b4a1f5f40 | curl (common - benign) |
cd08e31494f9531f560d64c695473da9 | Python requests library |
Operational note: JA3 is a hunting tool, not a blocking rule. A malicious JA3 from Cobalt Strike can be trivially changed by recompiling the implant. However, most operators don't bother - matching known-bad JA3s catches a significant portion of commodity attackers.
JA3S - Server TLS Fingerprint
JA3S fingerprints the server's ServerHello:
JA3S = MD5( SSLVersion, Cipher, Extensions )
By combining JA3 (client) + JA3S (server), you can fingerprint the communication pair - a specific C2 framework talking to its specific server, regardless of domain or IP.
JARM - Active Server Fingerprinting
JARM sends 10 specially crafted ClientHello packets to a server and hashes how the server responds. This fingerprints the server-side TLS stack. Identical JARM hashes across different IPs/domains indicate the same server software and configuration - useful for finding C2 infrastructure at scale.
# Install JARM
git clone https://github.com/salesforce/jarm
cd jarm
# Fingerprint a single server
python3 jarm.py target.example.com -p 443
# Fingerprint multiple IPs (C2 infrastructure hunting)
for ip in $(cat suspicious_ips.txt); do
echo -n "$ip: "
python3 jarm.py $ip -p 443 2>/dev/null | awk '{print $NF}'
done
# Example JARM output format:
# 2ad2ad0002ad2ad00042d42d000000ad9bf51cc3f5a1e29eecb81d0c7b06eb
# This hash is deterministic for a given server TLS configuration
Known JARM hashes:
| JARM Hash (prefix) | Server Software |
|---|---|
2ad2ad0002ad2ad00042d42d000000... | Cobalt Strike Team Server |
07d14d16d21d21d07c42d41d00041d... | Metasploit listener |
29d29d00029d29d21c29d29d29d29d... | nginx default |
HASSH - SSH Client/Server Fingerprinting
HASSH is the SSH equivalent of JA3 - it hashes the Key Exchange Init message fields:
# Extract HASSH from a PCAP using zeek
zeek -r capture.pcap -C /opt/zeek/share/zeek/site/hassh/
cat ssh.log | zeek-cut ts id.orig_h id.resp_h hassh hasshServer
# Or use the standalone hassh tool
pip install hassh
hassh -r /captures/ssh_session.pcap
4. Encrypted Traffic Analysis Without Decryption
Even when payload decryption is impossible, encrypted traffic leaks significant behavioral information through side-channel metadata.
Flow Metadata Features
Every TCP/TLS session exposes:
- Flow duration - C2 beacons have characteristic durations (often short check-ins)
- Bytes sent / received - asymmetric flows suggest exfiltration (large upload) or file download
- Inter-arrival timing - regular beaconing has low jitter; human browsing has high jitter
- Packet size distribution - streaming video has different packet sizes than file transfer
- Number of packets - a TLS session with 3 packets is a probe; one with 10,000 is data transfer
- Session count per destination - many short sessions to one IP = beaconing pattern
Beaconing Detection
C2 frameworks beacon at regular intervals. Even through TLS, the timing signature is visible in conn.log. The following script computes inter-beacon jitter for all external hosts:
#!/usr/bin/env python3
# beacon_detect.py - Detect periodic outbound TLS connections (C2 beaconing)
# Input: Zeek conn.log (tab-separated)
import pandas as pd
import numpy as np
import sys
# Load conn.log - skip comment lines starting with #
df = pd.read_csv('conn.log', sep='\t', comment='#',
names=['ts','uid','src_ip','src_port','dst_ip','dst_port',
'proto','service','duration','src_bytes','dst_bytes',
'state','local_orig','local_resp','missed_bytes',
'history','orig_pkts','orig_ip_bytes','resp_pkts',
'resp_ip_bytes','tunnel_parents'],
low_memory=False)
df['ts'] = pd.to_numeric(df['ts'], errors='coerce')
df = df.dropna(subset=['ts'])
# Focus on outbound TLS (port 443) connections
tls = df[(df['dst_port'] == 443) & (df['proto'] == 'tcp')].copy()
# Group by source IP + destination IP pair
for (src, dst), group in tls.groupby(['src_ip', 'dst_ip']):
if len(group) < 5: # Need at least 5 connections to assess pattern
continue
times = sorted(group['ts'].values)
intervals = np.diff(times) # Time between consecutive connections
if len(intervals) == 0:
continue
mean_interval = np.mean(intervals)
std_interval = np.std(intervals)
jitter_ratio = std_interval / mean_interval if mean_interval > 0 else 999
# Low jitter + regular interval = beacon signature
# Legitimate browsing has high jitter; beacons have low jitter
if mean_interval < 600 and jitter_ratio < 0.2 and len(group) > 10:
print(f"[BEACON CANDIDATE]")
print(f" {src} -> {dst}:443")
print(f" Connections: {len(group)}")
print(f" Mean interval: {mean_interval:.1f}s ({mean_interval/60:.1f} min)")
print(f" Jitter ratio: {jitter_ratio:.3f} (< 0.2 = suspicious)")
print(f" Bytes sent: {group['src_bytes'].sum():,}")
print()
# Run beacon detection
python3 beacon_detect.py
# Example output:
# [BEACON CANDIDATE]
# 192.168.1.105 -> 185.220.101.42:443
# Connections: 48
# Mean interval: 60.2s (1.0 min)
# Jitter ratio: 0.031 (< 0.2 = suspicious)
# Bytes sent: 24,576
NetworkML - ML-Based ETA
Cisco's open-source NetworkML applies ML classifiers to flow features to identify application type and anomalies - without decryption:
# Install NetworkML
pip install networkml
# Run on a PCAP - outputs application classification per flow
networkml -p /captures/suspicious.pcap
# Key features used internally by NetworkML:
# - Packet size mean, std, min, max
# - Inter-arrival time stats
# - Flow byte ratio (upload/download)
# - Flags distribution (SYN, ACK, PSH, FIN counts)
# - Duration and total bytes
5. TLS Interception - MITM Proxies & SSL Inspection
When metadata analysis is insufficient, organizations deploy TLS inspection proxies - also called SSL inspection, SSL/TLS decryption, or MITM proxies. The proxy terminates the client's TLS session, inspects the plaintext, and re-encrypts to the server using its own certificate signed by a corporate CA trusted by all endpoints.
How SSL Inspection Works
Client --[TLS to proxy cert]--> Proxy --[TLS to real server cert]--> Server
|
Plaintext
inspection here
The proxy presents a forged certificate for the destination, signed by the corporate CA. Clients trust this CA (deployed via MDM/GPO), so no certificate error is shown. The server's real certificate is validated by the proxy.
Legal & Operational Considerations
SSL inspection on corporate devices for corporate traffic is generally permissible when:
- Employees are notified (AUP / acceptable use policy)
- Traffic to known sensitive endpoints (banking, healthcare, personal webmail) is excluded
- Compliance requirements (HIPAA, PCI-DSS, attorney-client) are respected via bypass lists
SSL inspection on personal devices or without disclosure creates significant legal exposure.
mitmproxy - Full-Featured TLS Proxy
# Install mitmproxy
pip install mitmproxy
# Start mitmproxy in transparent mode (requires iptables redirect)
mitmproxy --mode transparent \
--listen-host 0.0.0.0 \
--listen-port 8080 \
--save-stream-file /captures/decrypted.mitm # Save all flows
# Interactive console mode (press ? for help)
mitmproxy
# Non-interactive dump mode (all flows to stdout)
mitmdump -w /captures/flows.mitm \ # Write flows to file
--flow-detail 3 \ # Verbosity: 3 = full headers + body
-q # Quiet (no console output)
# Replay a saved flow file
mitmproxy -r /captures/flows.mitm
# Run a mitmproxy addon/script to extract credentials
mitmdump -s extract_creds.py -r /captures/flows.mitm
# iptables rules to redirect traffic to mitmproxy (transparent mode)
# Run on the gateway/router
iptables -t nat -A PREROUTING -i eth1 -p tcp --dport 80 -j REDIRECT --to-port 8080
iptables -t nat -A PREROUTING -i eth1 -p tcp --dport 443 -j REDIRECT --to-port 8080
# Allow mitmproxy itself to connect outbound
iptables -t nat -A OUTPUT -p tcp -m owner --uid-owner mitmproxyuser -j RETURN
mitmproxy addon - extract HTTP headers and bodies to JSON:
# addon_logger.py - log all HTTPS requests/responses to NDJSON
import json
import mitmproxy.http
class HTTPLogger:
def response(self, flow: mitmproxy.http.HTTPFlow):
entry = {
"url": flow.request.pretty_url,
"method": flow.request.method,
"req_headers": dict(flow.request.headers),
"req_body": flow.request.content.decode("utf-8", errors="replace")[:2048],
"status": flow.response.status_code,
"resp_headers":dict(flow.response.headers),
"resp_body": flow.response.content.decode("utf-8", errors="replace")[:4096],
}
with open("/tmp/mitm_log.ndjson", "a") as f:
f.write(json.dumps(entry) + "\n")
addons = [HTTPLogger()]
# Run with the addon
mitmdump -s addon_logger.py --mode transparent
Squid + SSL-Bump (Enterprise Proxy)
For high-throughput corporate environments, Squid with ssl-bump is the standard approach:
# /etc/squid/squid.conf (relevant TLS inspection sections)
# Define the corporate CA certificate used to forge certs
tls_outgoing_options cafile=/etc/squid/ssl/corporate-ca.pem
# SSL bump steps
ssl_bump peek step1 all # Peek at SNI before deciding
ssl_bump bump step2 !no_ssl_bump # Bump (intercept) unless in exclusion ACL
ssl_bump splice step2 no_ssl_bump # Splice (pass through) for excluded sites
# Exclusion list - don't inspect these (banking, health, etc.)
acl no_ssl_bump dstdomain .chase.com .bankofamerica.com .mychart.org
# TLS certificate generation
sslcrtd_program /usr/lib/squid/security_file_certgen \
-s /var/lib/squid/ssl_db -M 4MB
http_port 3128 ssl-bump \
generate-host-certificates=on \
dynamic_cert_mem_cache_size=4MB \
tls-cert=/etc/squid/ssl/corporate-ca.pem \
tls-key=/etc/squid/ssl/corporate-ca.key
6. Decryption with Pre-Master Secrets & SSLKEYLOGFILE
For post-hoc forensic decryption of captured TLS sessions, you don't need a proxy. If you can extract the session keys from the endpoint that participated in the session, you can decrypt any previously captured traffic for that session.
SSLKEYLOGFILE - The Standard Approach
Most TLS libraries (NSS, OpenSSL via wrappers, BoringSSL) support the SSLKEYLOGFILE environment variable. When set, the library writes the pre-master secret for every session to a file in NSS Key Log Format. Wireshark and Zeek both support this format natively.
# Enable key logging in Firefox or Chrome
SSLKEYLOGFILE=/tmp/tls_keys.log firefox &
SSLKEYLOGFILE=/tmp/tls_keys.log google-chrome --no-sandbox &
# Enable for curl (uses NSS or OpenSSL depending on build)
SSLKEYLOGFILE=/tmp/tls_keys.log curl https://target.example.com
# Enable for Python (requests/urllib3)
# Note: Python's ssl module doesn't natively support SSLKEYLOGFILE
# Use the keylog_callback approach:
import ssl, os
def keylog_callback(conn, line):
with open("/tmp/tls_keys.log", "a") as f:
f.write(line.decode() + "\n")
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.keylog_filename = "/tmp/tls_keys.log" # Python 3.8+
# or:
ctx.set_keylog_callback(keylog_callback) # lower-level
# Enable for Node.js
NODE_OPTIONS='--tls-keylog=/tmp/tls_keys.log' node app.js
Key log file format (NSS Key Log Format):
# SSL/TLS secrets log file, generated by NSS
CLIENT_RANDOM a3f2...8b1c <48-byte-premaster-secret-hex>
CLIENT_HANDSHAKE_TRAFFIC_SECRET a3f2...8b1c <secret>
SERVER_HANDSHAKE_TRAFFIC_SECRET a3f2...8b1c <secret>
CLIENT_TRAFFIC_SECRET_0 a3f2...8b1c <secret>
SERVER_TRAFFIC_SECRET_0 a3f2...8b1c <secret>
Decrypting in Wireshark
Edit -> Preferences -> Protocols -> TLS -> (Pre)-Master-Secret log filename
-> point to /tmp/tls_keys.log
After loading the key log, Wireshark will automatically decrypt TLS sessions whose Client Random matches an entry. The decrypted application data appears in the packet dissection.
# Command-line decryption with tshark
tshark -r /captures/encrypted.pcap \
-o "tls.keylog_file:/tmp/tls_keys.log" \ # Key log file
-Y 'http' \ # Show only decrypted HTTP after TLS strip
-T fields \
-e http.request.method \
-e http.request.uri \
-e http.request.full_uri
# Export decrypted HTTP objects (files, downloads)
tshark -r /captures/encrypted.pcap \
-o "tls.keylog_file:/tmp/tls_keys.log" \
--export-objects "http,/tmp/exported_objects/"
# Follow a decrypted TLS stream
tshark -r /captures/encrypted.pcap \
-o "tls.keylog_file:/tmp/tls_keys.log" \
-q \
-z "follow,tls,ascii,0" # Follow stream index 0
Decrypting in Zeek
# Configure Zeek to use key log file
zeek -r /captures/encrypted.pcap \
-C \
"Ssl::keylog_file=/tmp/tls_keys.log" \ # Pass key log via Zeek variable
local
# After decryption, http.log will contain full URIs, headers, and file hashes
# files.log will contain extracted files with MD5/SHA1
cat http.log | zeek-cut ts id.orig_h method host uri status_code
cat files.log | zeek-cut ts source mime_type filename md5 sha1
RSA Key Decryption (TLS 1.2 only, no PFS)
If the server uses RSA key exchange (no perfect forward secrecy), the server's private key alone is enough to decrypt all captured sessions:
# Decrypt using server private key in Wireshark
# Edit -> Preferences -> Protocols -> TLS -> RSA keys list -> Add:
# IP: server_ip, Port: 443, Protocol: http, Key File: /path/to/server.key
# Command-line with ssldump
ssldump -r /captures/encrypted.pcap \
-k /path/to/server-private.key \ # Server RSA private key
-d \ # Decode application data
-A # Show all records
# Important: This ONLY works if the captured session used RSA key exchange.
# ECDHE/DHE (PFS) sessions cannot be decrypted from the private key alone.
# Check cipher suite: if "ECDHE" or "DHE" in name -> need key log file instead.
7. Certificate Intelligence & PKI Abuse
Even without payload decryption, TLS certificates are a rich intelligence source - and a common attacker staging point.
What Certificates Reveal
# Extract and parse certificate from live connection
openssl s_client -connect suspicious-domain.xyz:443 \
-servername suspicious-domain.xyz 2>/dev/null | \
openssl x509 -noout -text 2>/dev/null | \
grep -E "Subject:|Issuer:|Not Before|Not After|DNS:|IP Address:"
# Bulk certificate inspection from Zeek ssl.log
# Fields: ts, uid, src_ip, dst_ip, version, cipher, subject, issuer, validity
cat ssl.log | zeek-cut ts id.orig_h id.resp_h \
subject issuer validation_status | \
grep -v "Let's Encrypt\|DigiCert\|GlobalSign\|Comodo" | \ # Filter out common legit CAs
head -50 # Remaining = self-signed or unusual
# Find self-signed certificates in Zeek ssl.log
cat ssl.log | zeek-cut ts id.orig_h id.resp_h subject issuer | \
awk '$4 == $5' # Subject == Issuer -> self-signed
# Find certificates with very short validity (common in C2 infrastructure)
cat ssl.log | zeek-cut ts id.orig_h id.resp_h \
notvalidbefore notvalidafter | \
awk '{
split($4, a, "T"); split($5, b, "T");
diff = mktime(b[1]) - mktime(a[1]);
if (diff < 86400*30) print $0, "VALIDITY:", diff/86400, "days" # < 30 days
}'
Certificate Transparency (CT) Logs for Threat Intelligence
Attackers register certificates before launching attacks. CT logs record all publicly trusted certificates - monitoring them for your domains and lookalike domains provides early warning of phishing infrastructure.
# Query crt.sh for certificates issued for a domain (passive recon)
curl -s "https://crt.sh/?q=%.target-corp.com&output=json" | \
jq -r '.[] | [.logged_at, .name_value, .issuer_name] | @csv' | \
sort -r | head -50
# Find lookalike/typosquat domains recently certified
curl -s "https://crt.sh/?q=target-c0rp.com&output=json" | jq -r '.[].name_value'
# certstream - real-time CT log monitoring (detects phishing certs as issued)
pip install certstream
certstream --json | \
python3 -c "
import sys, json, re
for line in sys.stdin:
try:
d = json.loads(line)
if d.get('message_type') != 'certificate_update': continue
domains = d['data']['leaf_cert']['all_domains']
for domain in domains:
if re.search(r'(paypal|amazon|microsoft|google|facebook)', domain, re.I):
if not domain.endswith(('.paypal.com','.amazon.com','.microsoft.com')):
print('[PHISHING CANDIDATE]', domain)
except: pass
"
Identifying C2 via Certificate Attributes
Attacker-generated certificates, particularly those auto-generated by Cobalt Strike, Metasploit, or custom implants, often have characteristic fields:
# Cobalt Strike default certificate subject (very common IOC)
# Subject: C=US, ST=Washington, L=Redmond, O=Microsoft Corporation, CN=microsoft.com
# But issued by: Let's Encrypt or self-signed - mismatch!
# Detect Microsoft/Apple/Google certificate subject impersonation
cat ssl.log | zeek-cut ts id.orig_h id.resp_h subject issuer | \
grep -i "microsoft\|apple\|google\|amazon" | \ # Subject claims to be big tech
grep -vi "digicert\|verisign\|baltimore\|amazon trust" # But not signed by their CA
# Extract Cobalt Strike default cert fingerprint
# SHA1: 6aea28b9ba0e274f4f9a5c8d4c1a9e16e7c9a7f2 (varies by version, google current IOCs)
cat ssl.log | zeek-cut ts id.orig_h id.resp_h cert_chain_fps | \
grep "known_cs_sha1_here"
8. Tools Walkthrough - Zeek, Wireshark, mitmproxy, Suricata
Zeek - TLS/SSL Log Analysis Pipeline
# Full Zeek TLS analysis pipeline on a PCAP
zeek -r /captures/suspicious.pcap -C local
# ssl.log fields relevant to TLS analysis:
# ts, uid, id.orig_h, id.resp_h, version, cipher, curve, server_name (SNI),
# resumed, last_alert, next_protocol (ALPN), established, cert_chain_fps,
# client_cert_chain_fps, subject, issuer, validation_status, ja3, ja3s
# Find all unique JA3 hashes with associated SNIs
cat ssl.log | zeek-cut ja3 server_name | sort | uniq -c | sort -rn
# Find TLS sessions that failed validation (self-signed, expired, wrong hostname)
cat ssl.log | zeek-cut ts id.orig_h id.resp_h server_name validation_status | \
grep -v "^-\|ok"
# Correlate ssl.log with conn.log to add bytes/duration
join -1 2 -2 2 \
<(sort -k2 ssl.log) \
<(sort -k2 conn.log) | \
awk '{print $1, $3, $4, $14, $15, $7}' # ts, src, dst, duration, bytes, SNI
# Find connections where SNI doesn't match the certificate subject (domain fronting indicator)
cat ssl.log | zeek-cut server_name subject | \
awk '{
sni=$1;
sub(/CN=/, "", $2); cert_cn=$2;
if (sni != "" && cert_cn != "" && index(cert_cn, sni) == 0)
print "SNI:", sni, "CERT CN:", cert_cn
}'
Wireshark - TLS Dissection & Display Filters
# Capture only TLS traffic
tcpdump -i eth0 -w /tmp/tls.pcap 'tcp port 443 or tcp port 8443'
# Useful Wireshark display filters for TLS analysis:
# tls.handshake.type == 1 -> ClientHello only
# tls.handshake.type == 2 -> ServerHello only
# tls.handshake.extensions_server_name -> Has SNI field
# tls.alert_message -> TLS alerts (connection failures)
# tls.handshake.ciphersuite == 0x009c -> AES-128-GCM (check for weak suites)
# tls.record.version == 0x0301 -> SSLv3/TLS1.0 (deprecated, flag this)
# Command-line: extract all SNI values from a PCAP
tshark -r /captures/traffic.pcap \
-Y 'tls.handshake.extensions_server_name' \
-T fields \
-e ip.src \
-e ip.dst \
-e tls.handshake.extensions_server_name \
-E separator=, | sort | uniq -c | sort -rn
# Extract cipher suites offered by clients (fingerprinting)
tshark -r /captures/traffic.pcap \
-Y 'tls.handshake.type == 1' \
-T fields \
-e ip.src \
-e tls.handshake.ciphersuites \
-E separator="|"
Suricata - TLS-Aware Rules
# Detect TLS to a non-standard port (potential C2 tunneling)
alert tls $HOME_NET any -> $EXTERNAL_NET !443 (
msg:"TLS on Non-Standard Port - Possible C2 Tunnel";
flow:established,to_server;
tls.sni; content:!"."; # SNI either absent or unusual
classtype:policy-violation;
sid:9002001; rev:1;
)
# Detect expired TLS certificate
alert tls any any -> $HOME_NET any (
msg:"TLS Certificate Expired";
tls.cert_subject;
tls_cert_expired; # Suricata keyword: flags expired certs
classtype:policy-violation;
sid:9002002; rev:1;
)
# Detect TLS to known-bad JA3 (Cobalt Strike default)
alert tls $HOME_NET any -> $EXTERNAL_NET any (
msg:"Known Malicious JA3 - Cobalt Strike Default";
ja3.hash; content:"72a589da586844d7f0818ce684948eea";
classtype:command-and-control;
sid:9002003; rev:1;
metadata:mitre_tactic Command_And_Control, mitre_technique T1071.001;
)
# Detect domain fronting: SNI doesn't match Host header (HTTP/2 inside TLS)
# Requires TLS inspection or HTTP/2 parsing
alert http $HOME_NET any -> $EXTERNAL_NET any (
msg:"Possible Domain Fronting - SNI/Host Mismatch";
flow:established,to_server;
http.host; content:!"cloudfront.net"; # Example: real Host is different from SNI
# Combine with JA3 hunting for higher fidelity
classtype:policy-violation;
sid:9002004; rev:1;
)
9. Evasion - Domain Fronting, ESNI/ECH & Certificate Mimicry
Attackers don't passively accept TLS inspection. The following techniques are actively used to evade detection, inspection, and blocking.
Domain Fronting
Domain fronting exploits CDN infrastructure (Cloudflare, Fastly, Akamai) where the SNI in the TLS ClientHello points to an allowed domain (e.g., allowed.cloudflare.com) but the HTTP Host header inside the encrypted payload points to the actual C2 domain. The CDN routes based on Host header, not SNI.
Defender sees: SNI = allowed.cloudflare.com -> looks legitimate
Reality: Host: c2.attacker.com -> CDN routes to attacker's origin
Detection:
- Correlate SNI with DNS lookups - if the resolved IP belongs to a CDN and the content pattern is unusual, investigate
- Monitor for large asymmetric byte flows to CDN IPs (data exfiltration)
- After TLS inspection: compare SNI to HTTP Host header directly
# Detect domain fronting in mitmproxy (after decryption)
# addon_fronting.py
import mitmproxy.http
class DomainFrontingDetector:
def request(self, flow: mitmproxy.http.HTTPFlow):
sni = flow.client_conn.tls_extensions.get("server_name", "")
host = flow.request.headers.get("host", "")
if sni and host and sni.lower() != host.lower():
print(f"[DOMAIN FRONTING] SNI={sni} HOST={host} URL={flow.request.url}")
ESNI / ECH - Encrypted Client Hello
ESNI (Encrypted SNI) and its successor ECH (Encrypted Client Hello) encrypt the entire ClientHello, hiding the SNI from passive observers. The client fetches an ECHConfig via DNS (HTTPS record type), then encrypts the inner ClientHello using the server's public key.
# Check if a server supports ECH
dig HTTPS cloudflare.com # Look for "ech=" parameter in response
dig HTTPS crypto.cloudflare.com +short
# Test ECH with curl (requires recent build with ECH support)
curl --ech hard https://crypto.cloudflare.com/ # Fail if no ECH support
curl --ech grease https://target.com/ # Send fake ECH (GREASE)
# Detect ECH usage in Wireshark:
# TLS ClientHello -> Extensions -> encrypted_client_hello (type 0xFE0D)
tshark -r capture.pcap \
-Y 'tls.handshake.extensions.type == 65037' \ # 0xFE0D = ECH
-T fields -e ip.src -e ip.dst
Defensive implication: ECH breaks passive SNI-based filtering. Networks relying solely on SNI to block categories of traffic (gambling, social media) will be bypassed. The only reliable enforcement point becomes DNS-layer filtering (block the HTTPS DNS record) or full TLS inspection.
Certificate Mimicry
Sophisticated C2 frameworks generate certificates that mimic legitimate services:
- Issue a cert with
CN=microsoft.comsigned by a self-signed CA named "Microsoft IT TLS CA" - Register a typosquat domain (
micros0ft.com) and get a legitimate Let's Encrypt cert for it - Use a compromised legitimate domain as a staging server
# Detect certificate subject impersonation
# Look for certs claiming to be Microsoft/Google/Apple but signed by unknown CAs
cat ssl.log | zeek-cut subject issuer | \
awk '
/[Mm]icrosoft/ && !/DigiCert|Baltimore|GlobalSign/ { print "IMPERSONATION:", $0 }
/[Gg]oogle/ && !/Google Trust Services|GTS/ { print "IMPERSONATION:", $0 }
/[Aa]pple/ && !/Apple/ { print "IMPERSONATION:", $0 }
'
# Find Let's Encrypt certs on non-standard ports (unusual for legitimate services)
cat ssl.log | zeek-cut id.resp_p issuer | \
awk '$1 != 443 && /Let.s Encrypt/ { print "LE_NONSTANDARD_PORT:", $0 }'
Protocol Tunneling Through TLS
Attackers tunnel arbitrary protocols (SSH, RDP, DNS, custom binary) through TLS on port 443 to bypass firewalls:
# Detect non-HTTP traffic on port 443 via ALPN analysis
cat ssl.log | zeek-cut id.orig_h id.resp_h next_protocol server_name | \
awk '$3 != "h2" && $3 != "http/1.1" && $3 != "-" { print "UNUSUAL ALPN:", $0 }'
# Detect high-entropy TLS payloads (encrypted data exfil or custom protocol)
# Using entropy analysis on TLS Application Data records in tshark:
tshark -r /captures/suspicious.pcap \
-Y 'tls.app_data' \
-T fields \
-e ip.src -e ip.dst -e tls.app_data.len \
| awk '$3 > 16000' # Large TLS records (> ~16KB max) = bulk data transfer
10. MITRE ATT&CK Mapping
| Technique | ID | Description | Detection Method |
|---|---|---|---|
| Encrypted Channel: Asymmetric Cryptography | T1573.002 | TLS for C2 comms | JA3/JARM fingerprinting, flow metadata |
| Application Layer Protocol: Web Protocols | T1071.001 | HTTPS C2 | Beaconing detection, cert anomalies |
| Domain Fronting | T1090.004 | CDN-based evasion | SNI/Host header mismatch post-inspection |
| Proxy: External Proxy | T1090.002 | Route C2 through proxy | Chained TLS, unusual ALPN |
| Exfiltration Over C2 Channel | T1041 | Data exfil in TLS | Asymmetric byte flow analysis |
| Steal Web Session Cookie | T1539 | Via TLS MITM | Detect MITM proxy anomalies |
| Adversary-in-the-Middle | T1557 | TLS interception | Certificate issuer anomalies |
| DNS over HTTPS | T1071.004 | DoH for C2 | Traffic to 1.1.1.1/dns-query, 8.8.8.8/dns-query |
End of Chapter 2.4 - Encrypted Traffic Analysis & TLS Inspection
Next: Module 3 - Offensive Security & Exploitation Chapter 3.1: Reconnaissance, Scanning & Enumeration