Detecting TLS Fingerprint Anomalies via JA3/JA3S Computation and Malware Client Identification
Theory
Why This Matters
TLS fingerprinting was central to identifying Cobalt Strike beacon traffic in several high-profile breach investigations, including the detection of HAFNIUM activity during the 2021 Exchange Server exploitation wave. JA3 fingerprints computed from Cobalt Strike's default TLS ClientHello matched published threat intelligence, allowing defenders to identify C2 traffic even when destination IPs and domains changed daily. TLS fingerprinting lets an analyst classify and attribute TLS clients without decrypting traffic — the ClientHello parameters visible in every TLS handshake reliably distinguish browser versions, operating systems, malware families, and custom tools.
Core Concept
JA3 is a TLS client fingerprinting method that produces an MD5 hash from five fields extracted from the TLS ClientHello message:
- TLS Version (from the ClientHello record version field)
- Cipher Suites (ordered list of supported cipher suite codes, excluding GREASE values)
- Extensions (ordered list of extension type codes, excluding GREASE)
- Elliptic Curves (supported_groups extension values)
- EC Point Formats (ec_point_formats extension values)
The raw string is: TLSVersion,CipherSuites,Extensions,EllipticCurves,ECPointFormats with values comma-separated within each field and fields separated by dashes. JA3 = MD5 of this string.
JA3S fingerprints the TLS ServerHello response: TLSVersion,CipherSuite,Extensions (single cipher, not a list). A JA3+JA3S pair uniquely identifies a client-server TLS negotiation.
GREASE (Generate Random Extensions And Sustain Extensibility) values (0x0A0A, 0x1A1A, etc.) are inserted by browsers to test server compatibility. They must be excluded from JA3 computation.
Unusual JA3 hashes not matching known browser, OS, or legitimate application fingerprints indicate: - Malware/RATs: tools like Cobalt Strike, Metasploit, and njRAT have distinctive JA3 values published in threat intelligence. - Custom tools: non-standard cipher ordering or unusual extension sets. - Old/outdated clients: deprecated cipher suites signal legacy TLS implementations.
Technical Deep-Dive
# Compute JA3 hashes from a PCAP using the ja3 command-line tool
# pip install pyja3 or go install github.com/salesforce/ja3@latest
ja3 -a capture.pcap
# tshark: extract raw ClientHello fields for manual JA3 computation
tshark -r capture.pcap
-Y "tls.handshake.type == 1"
-T fields
-e frame.number -e ip.src -e ip.dst
-e tls.handshake.version
-e tls.handshake.ciphersuite
-e tls.handshake.extension.type
-e tls.handshake.extensions_supported_group
-e tls.handshake.extensions_ec_point_format
-E header=y -E separator="|"
# Extract JA3 hashes using zeek (if zeek is available)
# zeek -r capture.pcap policy/protocols/ssl/ssl-log-ext.zeek
# then: zeek-cut ja3 ja3s < ssl.log | sort | uniq -c | sort -rn
#!/usr/bin/env python3
"""
Compute JA3 fingerprint from tshark-extracted ClientHello fields.
GREASE values (0x?a?a pattern) are filtered before hashing.
"""
import hashlib, re
GREASE = {0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a, 0x5a5a,
0x6a6a, 0x7a7a, 0x8a8a, 0x9a9a, 0xaaaa, 0xbaba,
0xcaca, 0xdada, 0xeaea, 0xfafa}
def filter_grease(values):
return [v for v in values if v not in GREASE]
def parse_hex_list(field_str):
"""Parse tshark hex field like '0x0303,0x1301,0x1302' into int list."""
if not field_str or field_str == "(empty)":
return []
parts = re.findall(r"[0-9a-fA-F]+", field_str)
return [int(p, 16) for p in parts]
def compute_ja3(tls_version, cipher_suites, extensions,
elliptic_curves, ec_point_formats):
cs = filter_grease(cipher_suites)
ext = filter_grease(extensions)
ec = filter_grease(elliptic_curves)
ecpf = ec_point_formats # not filtered for GREASE
raw = (
f"{tls_version},"
f"{'-'.join(str(v) for v in cs)},"
f"{'-'.join(str(v) for v in ext)},"
f"{'-'.join(str(v) for v in ec)},"
f"{'-'.join(str(v) for v in ecpf)}"
)
ja3 = hashlib.md5(raw.encode()).hexdigest()
return raw, ja3
# Example: manually computed from tshark output
raw_str, ja3_hash = compute_ja3(
tls_version = 771, # 0x0303 = TLS 1.2
cipher_suites = parse_hex_list("0xc02b,0xc02f,0xc00a"),
extensions = parse_hex_list("0x0000,0x0017,0x000d"),
elliptic_curves = parse_hex_list("0x001d,0x0017,0x0018"),
ec_point_formats= [0], # uncompressed only
)
print(f"JA3 raw : {raw_str}")
print(f"JA3 hash: {ja3_hash}")
# Look up JA3 hash in public threat intelligence databases
# ja3er.com API (no live access needed for static lookup):
# https://ja3er.com/search/<ja3_hash>
# Known malware JA3 hashes (examples for reference — not flag values):
# Cobalt Strike default: 72a589da586844d7f0818ce684948eea (before profile change)
# Metasploit: de350869b8c85de67a350c8d186f11e6
# Zeek ssl.log ja3 field pivot
# cat ssl.log | zeek-cut ts uid src_ip dest_ip ja3 ja3s server_name
# | awk '$5 != "-"' | sort -k5 | uniq -f4 -c | sort -rn
Analytical Methodology
- Extract all TLS ClientHello messages from the PCAP using filter
tls.handshake.type == 1. For each, note source IP, destination IP, server name (SNI from extension), and the handshake parameters. - Compute or extract JA3 hashes for each distinct source IP. Group by JA3 hash to identify clients sharing the same TLS fingerprint — identical JA3 from different IPs may indicate botnet nodes or cloned implants.
- Compare computed JA3 hashes against known-malware databases (ja3er.com, Salesforce JA3 GitHub repository, Recorded Future, VirusTotal). A match is a high-confidence malware indicator.
- For unknown JA3 hashes, characterise the fingerprint: examine the cipher suite list order (browsers follow a specific preference order that reflects OS and version), extension set (browsers always include SNI, ALPN, session_ticket; custom tools often omit these), and supported groups (modern browsers include X25519, P-256, P-384).
- Identify anomalous fingerprints: single cipher suite in ClientHello (browser never does this), no SNI extension (normal browsers always include SNI for named hosts), unusual or deprecated cipher suites (RC4, 3DES), or GREASE values absent (some implementations do not implement GREASE — narrows the client library).
- Correlate anomalous JA3 sources with other traffic: what destinations do they connect to? Are those destinations newly registered domains, known malware C2 IPs, or cloud infrastructure? Do the connections show beaconing intervals?
- Compute JA3S for the server responses to anomalous clients. A known-malicious JA3S (e.g., Cobalt Strike's default server fingerprint) alongside an anomalous JA3 confirms C2 communication.
- Document: JA3 hash, raw JA3 string (for reproducibility), source IP, matched threat intelligence (if any), characterisation of anomaly, and correlated indicators.
Common Analytical Errors
- Forgetting to filter GREASE values: Including GREASE values in JA3 computation produces hashes that do not match published databases, because databases were computed with GREASE filtered. Always exclude GREASE before hashing.
- Treating JA3 as definitive malware proof: JA3 is a probabilistic indicator. JA3 hash collisions occur — a legitimate application may share a hash with known malware. JA3 is a triage tool, not conclusive attribution. Always corroborate with behavioural indicators.
- Missing TLS 1.3 ClientHello differences: TLS 1.3 ClientHellos include a
supported_versionsextension with the actual TLS version (0x0304), while the record layer version may show TLS 1.2 for backwards compatibility. Use thesupported_versionsextension value for correct JA3 computation with TLS 1.3 clients. - Overlooking JA3S for server fingerprinting: Most analysts compute JA3 (client) but forget JA3S (server). Known C2 frameworks have distinctive ServerHello responses — specific cipher selection, characteristic extension order. Computing JA3S identifies server-side infrastructure fingerprints.
NICE Framework Alignment
| Code | Knowledge/Skill/Task Statement | How This Card Develops It |
|---|---|---|
| K0046 | Knowledge of intrusion detection systems and methodologies | JA3 fingerprinting is a modern passive IDS technique for encrypted traffic threat detection |
| K0093 | Knowledge of network protocols | TLS ClientHello structure, extension semantics, cipher suite codes, and GREASE mechanism |
| K0221 | Knowledge of OSI model and network layers | TLS handshake occurs at layer 6/7; JA3 extracts layer-6 negotiation parameters visible without decryption |
| S0046 | Skill in performing packet-level analysis | Extracting TLS handshake fields from PCAP and computing JA3 hashes for threat intelligence correlation |
| T0023 | Collect intrusion artifacts for use in forensic analysis | JA3 hashes are reproducible forensic artifacts enabling tool attribution from encrypted traffic |
Further Reading
- Salesforce Engineering: "TLS Fingerprinting with JA3 and JA3S" (original JA3 blog post)
- ja3er.com: community JA3 hash database with client identification
- Zeek JA3 plugin documentation: zeek.org
- SANS: "Hunting for TLS Malware with JA3 and JA3S" (ISC diary)
- Recorded Future: "Cobalt Strike JA3 Fingerprints" threat intelligence report
Challenge Lab
Reinforce your learning with a hands-on generated challenge based on this card's competency.