TLS Deep Dive
TLS encrypts the application payload but leaves substantial forensic evidence in the handshake: the TLS version negotiated, cipher suites offered and selected, the SNI extension (the hostname the client wants to connect to), the server's X.509 certificate (subject, issuer, SANs, validity period), and the TLS extensions list which forms the basis for JA3 fingerprinting. Together these fields characterize TLS sessions without decrypting a single byte of payload.
You're analyzing HTTPS traffic from a suspected Cobalt Strike infection. Every connection is TLS-encrypted but you notice: all ClientHello packets offer the exact same cipher suites in the exact same order, with extensions in an unusual sequence that doesn't match any browser. The JA3 hash computed from these fields matches known Cobalt Strike signatures in public threat intel databases. You've identified the C2 traffic without decrypting anything.
TLS 1.2 Handshake — Forensic Anatomy
TLS 1.2 Handshake — What Each Message Reveals
═══════════════════════════════════════════════════════════════════
Client → Server: ClientHello [FORENSIC GOLDMINE]
├── TLS version: 0x0303 (TLS 1.2) / 0x0301 (TLS 1.0, old/malware)
├── Random: 32 bytes (not useful directly)
├── Session ID: resumes previous session (length = 0 if new)
├── Cipher Suites: ordered list the client supports
│ Normal browser: 15-25 suites in specific order
│ Malware: often 1-5 suites or unusual order
├── Compression Methods: 0x00 (none) — any other value = old/broken
└── Extensions:
server_name (0x0000): the SNI — hostname client wants
supported_groups (0x000a): EC curves supported
session_ticket (0x0023): enable TLS session tickets
supported_versions (0x002b): TLS 1.3 if offered
signature_algorithms (0x000d): hash+sig combos accepted
extended_master_secret (0x0017): security extension
encrypt_then_mac (0x0016): security extension
Server → Client: ServerHello
├── Selected TLS version
├── Selected cipher suite
└── Selected extensions
Server → Client: Certificate [FORENSIC GOLDMINE]
├── Subject CN: who the cert claims to be
├── Subject Alternative Names (SANs): all valid hostnames
├── Issuer CN: which CA issued the cert
├── Serial number: unique identifier
├── Not Before / Not After: validity window
│ Newly issued cert for a session = possible C2
│ Self-signed = no CA verification = C2 indicator
└── Public key algorithm and size
ServerHello + Certificate → Client is visible even in encrypted TLS 1.3
(Certificate is encrypted in TLS 1.3 but ServerHello is plaintext)
TLS Metadata Extraction
#!/bin/bash
PCAP="$1"
echo "=== TLS ClientHello analysis ==="
tshark -r "$PCAP" -n -Y "tls.handshake.type==1" -T fields \
-E separator="\t" \
-e frame.time_epoch \
-e ip.src \
-e ip.dst \
-e tcp.dstport \
-e tls.handshake.extensions_server_name \
-e tls.handshake.version \
-e tls.handshake.ciphersuite \
| head -50
echo ""
echo "=== Server certificates (subject + issuer) ==="
tshark -r "$PCAP" -n -Y "tls.handshake.type==11" -T fields \
-E separator="\t" \
-e ip.src \
-e ip.dst \
-e tls.handshake.certificate_length \
-e x509ce.dNSName \
-e tls.handshake.extensions_server_name \
| head -30
echo ""
echo "=== Self-signed certificates (issuer == subject) ==="
tshark -r "$PCAP" -n \
-Y "tls.handshake.type==11" \
-T json \
| python3 -c "
import json, sys
data = json.load(sys.stdin)
for pkt in data:
try:
layers = pkt['_source']['layers']
tls = layers.get('tls', {})
# Look for self-signed indicator: same issuer and subject RDN
cert = str(tls)
if 'tls.x509' in cert.lower():
print(str(pkt['_source']['layers'].get('ip', {}).get('ip.dst',''))[:50])
except:
pass
" 2>/dev/null | sort -u | head -20
echo ""
echo "=== TLS versions negotiated (old versions = red flag) ==="
tshark -r "$PCAP" -n -Y "tls.handshake.type==2" -T fields \
-e tls.record.version -e tls.handshake.version \
| sort | uniq -c | sort -rn
echo ""
echo "=== Connections with no SNI (C2 indicator) ==="
tshark -r "$PCAP" -n \
-Y "tls.handshake.type==1 and not tls.handshake.extensions_server_name" \
-T fields \
-E separator="\t" \
-e ip.src \
-e ip.dst \
-e tcp.dstport \
| sort | uniq -c | sort -rn | head -20
Certificate Forensics
| Certificate Attribute | Legitimate | C2 / Malicious |
|---|---|---|
| Issuer | Known CA (DigiCert, Let's Encrypt, Sectigo) | Self-signed, unknown CA, or attacker-controlled CA |
| Subject CN | Matches SNI sent by client | Generic (example.com), mismatched, or IP address |
| SANs | Multiple relevant hostnames | One or none; wildcard for single domain |
| Validity period | 1–2 years typical | Very long (10 years) or very short (days/hours) |
| Issue date | Stable infrastructure, weeks to months old | Recently issued (last 7 days) for a session |
| Public key size | 2048+ RSA or 256-bit EC | May use 1024-bit RSA (old, weak) |
| Let's Encrypt certs | Legitimate domains passing ACME validation | Attackers also use LE — free, automated, trusted by all |
Let's Encrypt is used extensively by attackers because it's free, automated, and trusted by every browser and OS in the world. An attacker who registers microsoft-update-service.com and gets a Let's Encrypt cert has a perfectly valid HTTPS connection that will never trigger a certificate error. The issuer being Let's Encrypt is not a signal of legitimacy — it's table stakes for any competent attacker. What matters is the domain name, the SNI, and whether the IP hosting the cert matches what you expect. A Let's Encrypt cert on an IP with no rDNS, registered 3 days ago, hosted on a VPS provider, is suspicious regardless of who issued the cert.
Q & A
Q: In TLS 1.3, the certificate is encrypted. What forensic metadata is still visible?
TLS 1.3 encrypts more of the handshake than TLS 1.2, but significant metadata remains visible: (1) ClientHello is always plaintext: the full list of cipher suites, extensions, supported groups, and the SNI are visible unless the client uses Encrypted Client Hello (ECH), which is not yet widely deployed. (2) ServerHello is plaintext: the selected cipher suite and server random are visible. (3) Certificate in the handshake: in TLS 1.3, the Certificate and CertificateVerify messages are encrypted in the EncryptedExtensions record. However, many TLS 1.3 implementations still respond to OCSP stapling requests which reveal the certificate serial number. (4) JA3 still works: the ClientHello fields used for JA3 are present and visible in TLS 1.3 — the same fingerprinting technique applies. (5) Network metadata: source/destination IP, port, SNI, timing, and data volume remain visible. The payload is encrypted, but the connection metadata is not.