← News
/PLAYBOOK · 2026-08-02

JA3 Fingerprinting in Practice: Detect Malicious TLS Client Traffic

Principle-to-practice JA3: extract, match, and operationalize with tshark, Python, and Suricata to detect malicious TLS clients.

JA3 fingerprints TLS ClientHello without decrypting traffic. Use it to spot malware, scanners, and crawlers. This walkthrough extracts JA3 with tshark, matches public blacklists in Python, and alerts in Suricata.

Use cases

Prerequisites

How JA3 works

Salesforce (2017). ClientHello fields — SSL/TLS version, cipher suites, extensions, elliptic curves, EC point formats — are concatenated in a fixed order and MD5-hashed to a 32-hex fingerprint, e.g. `51c64c77e60f3980eea90869b68c58a8`.

Same software / TLS library → same JA3 across platforms. Browsers, OS stacks, and malware differ. JA3S fingerprints ServerHello.

Procedure

Step 1: Extract JA3 with tshark

# Extract JA3 and source IP from every TLS ClientHello in a pcap
tshark -r capture.pcap -Y "tls.handshake.type==1"   -T fields -e tls.handshake.ja3 -e ip.src -E separator=,

# Live capture, top 20 fingerprints
tshark -i eth0 -Y "tls.handshake.type==1"   -T fields -e tls.handshake.ja3 -e ip.src |   sort | uniq -c | sort -rn | head -20

Step 2: Match public malicious fingerprints

# Query one hash (Ja3er)
curl -s "https://ja3er.com/search/51c64c77e60f3980eea90869b68c58a8"

# Download abuse.ch JA3 blacklist (format: ja3_hash,label)
curl -s "https://sslbl.abuse.ch/blacklist/ja3_fingerprints.csv" -o ja3_blacklist.csv

Step 3: Python batch detector

#!/usr/bin/env python3
import subprocess
from collections import Counter

BLACKLIST = {}
with open("ja3_blacklist.csv") as f:
    for line in f:
        if line.startswith("#") or not line.strip():
            continue
        parts = line.strip().split(",")
        if len(parts) >= 2:
            BLACKLIST[parts[0]] = parts[1]

out = subprocess.run(
    ["tshark", "-r", "capture.pcap", "-Y", "tls.handshake.type==1",
     "-T", "fields", "-e", "tls.handshake.ja3", "-e", "ip.src"],
    capture_output=True, text=True).stdout

hits = Counter()
for line in out.strip().splitlines():
    fields = line.split("\t")
    if len(fields) != 2:
        continue
    ja3, src = fields
    if ja3 in BLACKLIST:
        hits[(ja3, src)] += 1
        print(f"[HIT] {src} {ja3} {BLACKLIST[ja3]}")

print("\n=== Summary ===")
for (ja3, src), n in hits.most_common():
    print(f"{n}\t{src}\t{ja3}\t{BLACKLIST[ja3]}")

Step 4: Suricata ja3.rules

Write `ja3.rules`:

alert tls any any -> any any (msg:"Malicious JA3 fingerprint - botnet"; ja3.hash; content:"4c8e0ff8e8ee8f3f1b69d7b1d7f2a2d1"; sid:1100001; rev:1;)

alert tls any any -> any any (msg:"Suspicious JA3 fingerprint - known scanner"; ja3.hash; content:"51c64c77e60f3980eea90869b68c58a8"; sid:1100002; rev:1;)
# Load rules and restart Suricata
echo 'include ja3.rules' | sudo tee -a /etc/suricata/suricata.yaml
sudo suricata -T -c /etc/suricata/suricata.yaml
sudo systemctl restart suricata

Verify

# Check 1: compare JA3 across clients
curl -s -o /dev/null "https://www.baidu.com"
tshark -r capture2.pcap -Y "tls.handshake.type==1"   -T fields -e tls.handshake.ja3

# Check 2: confirm Suricata loaded JA3 rules and can hit
sudo suricatasc -c "rule-reload-noncached"
grep -c "ja3.hash" /var/log/suricata/eve.json

FAQ

FAQ1: JA3 vs JA3S?

JA3 = ClientHello (client). JA3S = ServerHello (server). Combine them: malware with a fixed JA3 talking to rotating C2 still correlates on JA3S as a second axis.

FAQ2: Can JA3 be spoofed?

Yes. Attackers retune the TLS stack or mimic browser fingerprints. JA3 is not a hard identity. Correlate with IP reputation, traffic behavior, and DNS. Baseline fingerprints and refresh the malicious set.

Summary

JA3 identifies TLS clients without decryption. Fit for IDS, traffic analysis, and threat intel. Maintain a baseline, refresh malicious hashes, and pair with behavior to cut false positives. On Nginx/web-only paths, use OpenResty's ssl-fingerprint module or a tap IDS.