JA4S: TLS Server Response Fingerprinting
Introduction
JA4S (JA4 Server) is a network fingerprinting technique that identifies TLS servers based on their Server Hello response during the TLS handshake. While JA4 fingerprints clients, JA4S provides the complementary server-side view, enabling detection of malicious servers, server impersonation, and infrastructure mapping.
Key Advantage: JA4S enables identification of servers even when clients vary, making it essential for detecting command-and-control (C2) servers, phishing infrastructure, and compromised servers.
Skill Level: Beginner to Intermediate
Prerequisites:
- Basic understanding of TLS/SSL handshakes
- Familiarity with JA4 client fingerprinting
- Basic network packet analysis skills
- Understanding of X.509 certificates
Learning Objectives:
- Understand TLS Server Hello structure
- Construct JA4S fingerprints from server responses
- Implement JA4S fingerprinting in Python
- Detect malicious servers and infrastructure
- Integrate JA4S with threat intelligence
Why JA4S Matters
Traditional server identification methods face challenges:
- IP addresses change frequently (CDNs, cloud hosting)
- Domain names can be quickly rotated
- Certificates can be stolen or forged
JA4S solves these problems by fingerprinting:
- Server's TLS version selection
- Chosen cipher suite
- Extension configuration
- Certificate attributes (using JA4X)
Real-World Applications
- C2 Server Detection: Identify command-and-control infrastructure
- Phishing Detection: Detect fake login pages and credential harvesters
- Server Impersonation: Identify servers masquerading as legitimate services
- Infrastructure Mapping: Map attacker infrastructure across IP changes
- Threat Intelligence: Track threat actor server configurations
Understanding JA4S Components
The JA4S Fingerprint Format
<protocol><version><alpn1><alpn2><cipher>_<extension_hash>Example: t120200_23ee972fd6b4_a55e728098b8
Component Breakdown
1. Protocol (1 char)
Transport protocol:
t= TLS over TCPq= QUICd= DTLS over UDP
2. TLS Version (2 chars)
Server-selected TLS version:
13= TLS 1.312= TLS 1.211= TLS 1.110= TLS 1.0s3= SSL 3.0d2= DTLS 1.2d3= DTLS 1.3
3. ALPN (2 chars each)
First 2 characters of first two ALPN values:
h2= HTTP/2h3= HTTP/3 (QUIC)ht= http/1.100= No ALPN
Format: <first_alpn><second_alpn>
- If only one ALPN:
h200 - If no ALPN:
0000
4. Chosen Cipher Suite (4 chars)
Server-selected cipher in hex:
1301= TLS_AES_128_GCM_SHA2561302= TLS_AES_256_GCM_SHA384c02f= TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
5. Extension Hash (12 chars)
SHA-256 hash of extension codes (sorted, first 12 chars)
Step-by-Step: Constructing a JA4S Fingerprint
Example TLS Server Hello
TLS Version: 0x0303 (TLS 1.2)
Cipher Suite: 0xc02f (TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256)
Extensions:
- renegotiation_info (0xff01)
- extended_master_secret (0x0017)
- session_ticket (0x0023)
- application_layer_protocol_negotiation (0x0010): h2, http/1.1Step 1: Extract Protocol and Version
- Protocol: TLS over TCP →
t - TLS Version: 0x0303 (TLS 1.2) →
12
Step 2: Extract ALPN Values
- First ALPN:
h2→h2 - Second ALPN:
http/1.1→ht(first 2 chars) - Combined:
h2ht
Step 3: Extract Cipher Suite
- Cipher: 0xc02f →
c02f
Step 4: Compute Extension Hash
Extensions in hex (sorted):
0010, 0017, 0023, ff01Python Implementation:
import hashlib
def compute_ja4s_extension_hash(extensions_list):
"""
Compute JA4S extension hash.
Args:
extensions_list: List of extension hex codes
Returns:
First 12 characters of SHA-256 hash
"""
# Sort extensions by hex value
sorted_extensions = sorted(extensions_list, key=lambda x: int(x, 16))
# Join with commas
extension_string = ','.join(sorted_extensions)
# Compute SHA-256 hash
hash_value = hashlib.sha256(extension_string.encode()).hexdigest()
# Return first 12 characters
return hash_value[:12]
# Example
extensions = ['ff01', '0017', '0023', '0010']
ext_hash = compute_ja4s_extension_hash(extensions)
print(f"Extension Hash: {ext_hash}") # Example: 23ee972fd6b4Step 5: Assemble the Fingerprint
t12h2htc02f_23ee972fd6b4Breakdown:
t= TLS over TCP12= TLS 1.2h2ht= ALPN: h2 and http/1.1c02f= Chosen cipher23ee972fd6b4= Extension hash
Complete Python Implementation
import hashlib
from typing import List, Optional, Dict
class JA4S:
"""JA4S TLS Server Fingerprinting Implementation"""
# Protocol mapping
PROTOCOL_MAP = {
'tls': 't',
'quic': 'q',
'dtls': 'd'
}
# TLS version mapping
VERSION_MAP = {
0x0304: '13', # TLS 1.3
0x0303: '12', # TLS 1.2
0x0302: '11', # TLS 1.1
0x0301: '10', # TLS 1.0
0x0300: 's3', # SSL 3.0
0xfeff: 'd1', # DTLS 1.0
0xfefd: 'd2', # DTLS 1.2
0xfefc: 'd3', # DTLS 1.3
}
@staticmethod
def compute_fingerprint(
protocol: str,
tls_version_hex: int,
alpn_list: Optional[List[str]],
cipher_suite: str,
extensions: List[str]
) -> str:
"""
Compute complete JA4S fingerprint.
Args:
protocol: 'tls', 'quic', or 'dtls'
tls_version_hex: TLS version as hex (e.g., 0x0303)
alpn_list: List of ALPN protocol strings
cipher_suite: Cipher suite hex code (4 chars)
extensions: List of extension hex codes
Returns:
JA4S fingerprint string
"""
# Protocol
proto_char = JA4S.PROTOCOL_MAP.get(protocol.lower(), 't')
# Version
version_code = JA4S.VERSION_MAP.get(tls_version_hex, '00')
# ALPN processing
if alpn_list and len(alpn_list) > 0:
alpn1 = alpn_list[0][:2].lower()
alpn2 = alpn_list[1][:2].lower() if len(alpn_list) > 1 else '00'
else:
alpn1 = '00'
alpn2 = '00'
# Cipher suite (already 4 chars hex)
cipher = cipher_suite.lower()
# Extension hash
sorted_extensions = sorted(extensions, key=lambda x: int(x, 16))
extension_string = ','.join(sorted_extensions)
ext_hash = hashlib.sha256(extension_string.encode()).hexdigest()[:12]
# Assemble fingerprint
fingerprint = (
f"{proto_char}{version_code}{alpn1}{alpn2}{cipher}_{ext_hash}"
)
return fingerprint
@staticmethod
def parse_server_hello(server_hello_data: Dict) -> Dict:
"""
Parse server hello data into components.
Args:
server_hello_data: Dict with parsed server hello fields
Returns:
Dict with parsed components
"""
return {
'protocol': server_hello_data.get('protocol', 'tls'),
'tls_version_hex': server_hello_data.get('version', 0x0303),
'alpn_list': server_hello_data.get('alpn', []),
'cipher_suite': server_hello_data.get('cipher', '0000'),
'extensions': server_hello_data.get('extensions', [])
}
# Example Usage
if __name__ == "__main__":
# Sample Server Hello data
server_hello = {
'protocol': 'tls',
'version': 0x0303, # TLS 1.2
'alpn': ['h2', 'http/1.1'],
'cipher': 'c02f',
'extensions': ['ff01', '0017', '0023', '0010']
}
# Parse and compute fingerprint
parsed = JA4S.parse_server_hello(server_hello)
ja4s_fp = JA4S.compute_fingerprint(
parsed['protocol'],
parsed['tls_version_hex'],
parsed['alpn_list'],
parsed['cipher_suite'],
parsed['extensions']
)
print(f"JA4S Fingerprint: {ja4s_fp}")
# Output: t12h2htc02f_23ee972fd6b4 (example)Capturing TLS Server Traffic
Using Wireshark
- Start Capture: Select network interface
- Filter:
tls.handshake.type == 2(Server Hello) - Analyze: Expand TLS → Handshake Protocol → Server Hello
- Extract: Version, Cipher, Extensions, ALPN
Using tcpdump
# Capture TLS traffic to specific server
sudo tcpdump -i eth0 'host example.com and port 443' -w server_hello.pcap
# Read and analyze
tcpdump -r server_hello.pcap -XUsing Python + Scapy
from scapy.all import sniff, TCP, TLS, Raw
import binascii
def process_server_hello(packet):
"""Extract Server Hello from TLS packet"""
if packet.haslayer('TLS'):
tls_layer = packet['TLS']
# Check for Server Hello
if hasattr(tls_layer, 'msg') and len(tls_layer.msg) > 0:
for msg in tls_layer.msg:
if msg.msgtype == 2: # Server Hello
print("="*60)
print("Server Hello Captured:")
print(f"TLS Version: {hex(msg.version)}")
print(f"Cipher Suite: {hex(msg.cipher)}")
if hasattr(msg, 'ext'):
print(f"Extensions: {[hex(e.type) for e in msg.ext]}")
# Compute JA4S
ja4s = compute_ja4s_from_packet(msg)
print(f"JA4S Fingerprint: {ja4s}")
print("="*60)
# Capture Server Hello packets
print("Capturing TLS Server Hello... Press Ctrl+C to stop")
sniff(filter="tcp port 443", prn=process_server_hello, store=0)Practical Applications
1. C2 Server Detection
Malware command-and-control servers often have distinctive JA4S fingerprints:
# Known C2 server fingerprints
C2_FINGERPRINTS = {
't12000013301_a1b2c3d4e5f6': {
'name': 'Cobalt Strike Default',
'severity': 'critical',
'description': 'Default Cobalt Strike C2 server configuration'
},
't1300001302_f6e5d4c3b2a1': {
'name': 'Metasploit Handler',
'severity': 'high',
'description': 'Metasploit default HTTPS handler'
},
't12h2001301_9876543210ab': {
'name': 'Custom Malware C2',
'severity': 'critical',
'description': 'Known malicious C2 infrastructure'
}
}
def check_c2_server(ja4s_fingerprint):
"""Check if JA4S matches known C2 servers"""
if ja4s_fingerprint in C2_FINGERPRINTS:
c2_info = C2_FINGERPRINTS[ja4s_fingerprint]
return {
'is_c2': True,
'name': c2_info['name'],
'severity': c2_info['severity'],
'description': c2_info['description']
}
return {'is_c2': False}
# Example usage
ja4s = "t12000013301_a1b2c3d4e5f6"
result = check_c2_server(ja4s)
if result['is_c2']:
print(f"ALERT: C2 Server Detected!")
print(f"Name: {result['name']}")
print(f"Severity: {result['severity']}")
print(f"Description: {result['description']}")2. Phishing Infrastructure Detection
Track phishing servers across IP rotations:
class PhishingTracker:
def __init__(self):
self.known_phishing_ja4s = set()
self.tracked_servers = {}
def add_phishing_fingerprint(self, ja4s, details):
"""Add known phishing server fingerprint"""
self.known_phishing_ja4s.add(ja4s)
self.tracked_servers[ja4s] = details
def check_server(self, ip_address, domain, ja4s):
"""Check if server matches known phishing infrastructure"""
if ja4s in self.known_phishing_ja4s:
details = self.tracked_servers[ja4s]
return {
'is_phishing': True,
'campaign': details['campaign'],
'first_seen': details['first_seen'],
'targets': details['targets']
}
return {'is_phishing': False}
# Usage
tracker = PhishingTracker()
# Add known phishing infrastructure
tracker.add_phishing_fingerprint(
't12h2htc02f_phishing123',
{
'campaign': 'Banking Phish 2024',
'first_seen': '2024-01-15',
'targets': ['bank1.com', 'bank2.com']
}
)
# Check new connection
result = tracker.check_server(
'192.168.1.100',
'fake-bank.com',
't12h2htc02f_phishing123'
)
if result['is_phishing']:
print(f"WARNING: Phishing server detected!")
print(f"Campaign: {result['campaign']}")
print(f"Block access immediately!")3. Server Impersonation Detection
Detect servers pretending to be legitimate services:
# Known legitimate server fingerprints
LEGITIMATE_SERVERS = {
'google.com': ['t13h2h3c1301_google123456', 't13h3h2c1302_google789abc'],
'microsoft.com': ['t13h2htc1301_msft123456', 't12h2htc02f_msft789abc'],
'github.com': ['t13h2htc1301_github12345']
}
def verify_server_authenticity(domain, ja4s_fingerprint):
"""Verify if server fingerprint matches known legitimate fingerprints"""
if domain in LEGITIMATE_SERVERS:
if ja4s_fingerprint in LEGITIMATE_SERVERS[domain]:
return {'authentic': True, 'confidence': 'high'}
else:
return {
'authentic': False,
'confidence': 'high',
'reason': 'Fingerprint does not match known legitimate servers'
}
return {'authentic': None, 'confidence': 'unknown'}
# Example
domain = "google.com"
ja4s = "t13h2h3c1301_suspicious12"
result = verify_server_authenticity(domain, ja4s)
if result['authentic'] == False:
print(f"WARNING: Possible server impersonation!")
print(f"Domain claims to be {domain} but has unexpected fingerprint")
print(f"Reason: {result['reason']}")Integration with Security Tools
Zeek/Bro Script
# JA4S extraction in Zeek
module JA4S;
export {
redef record SSL::Info += {
ja4s: string &optional &log;
};
}
event ssl_server_hello(c: connection, version: count, record_version: count,
possible_ts: time, server_random: string,
session_id: string, cipher: count, comp_method: count)
{
local ja4s_string = compute_ja4s(c, version, cipher);
c$ssl$ja4s = ja4s_string;
# Log JA4S fingerprint
Log::write(SSL::LOG, c$ssl);
# Check against threat intelligence
if (ja4s_string in known_malicious_ja4s) {
NOTICE([$note=SSL::Malicious_Server_Detected,
$msg=fmt("Malicious JA4S fingerprint: %s", ja4s_string),
$conn=c]);
}
}Suricata Rule
alert tls any any -> $HOME_NET any (msg:"Malicious C2 Server JA4S Detected";
tls.fingerprint; content:"t12000013301_a1b2c3d4e5f6";
classtype:trojan-activity;
sid:2000001; rev:1;)Splunk Query
index=network sourcetype=tls_logs
| eval ja4s=protocol.version.alpn.cipher."_".extension_hash
| stats count by ja4s, dest_ip, dest_port
| where ja4s IN (known_c2_fingerprints)
| table _time, src_ip, dest_ip, ja4s, threat_nameAdvanced Techniques
1. JA4S + JA4 Correlation
Correlate client and server fingerprints:
class ClientServerCorrelator:
def __init__(self):
self.connections = []
def add_connection(self, src_ip, dst_ip, ja4_client, ja4s_server):
"""Track client-server fingerprint pairs"""
self.connections.append({
'src_ip': src_ip,
'dst_ip': dst_ip,
'ja4': ja4_client,
'ja4s': ja4s_server,
'timestamp': datetime.now()
})
def find_suspicious_pairs(self):
"""Identify suspicious client-server fingerprint combinations"""
suspicious = []
for conn in self.connections:
# Example: Metasploit client + Metasploit server
if ('meterpreter' in conn['ja4'] and
'metasploit' in conn['ja4s']):
suspicious.append(conn)
# Example: Cobalt Strike patterns
if ('cobaltstrike' in conn['ja4'] and
'cobaltstrike' in conn['ja4s']):
suspicious.append(conn)
return suspicious
# Usage
correlator = ClientServerCorrelator()
correlator.add_connection(
'10.0.0.100',
'203.0.113.50',
't13d1516h2_meterpreter123',
't12000013301_metasploit456'
)
suspicious = correlator.find_suspicious_pairs()
for conn in suspicious:
print(f"ALERT: Suspicious C2 communication detected!")
print(f"Client: {conn['src_ip']} → Server: {conn['dst_ip']}")2. Temporal Analysis
Track server fingerprint changes over time:
from datetime import datetime, timedelta
from collections import defaultdict
class ServerFingerprintTracker:
def __init__(self):
self.server_history = defaultdict(list)
def track(self, server_identifier, ja4s, timestamp=None):
"""Track JA4S fingerprints for a server over time"""
if timestamp is None:
timestamp = datetime.now()
self.server_history[server_identifier].append({
'ja4s': ja4s,
'timestamp': timestamp
})
def detect_fingerprint_change(self, server_identifier, time_window_hours=24):
"""Detect if server fingerprint changed recently"""
history = self.server_history[server_identifier]
if len(history) < 2:
return False
cutoff = datetime.now() - timedelta(hours=time_window_hours)
recent = [h for h in history if h['timestamp'] > cutoff]
if len(recent) < 2:
return False
# Check if fingerprint changed
fingerprints = set(h['ja4s'] for h in recent)
if len(fingerprints) > 1:
return {
'changed': True,
'old_fingerprint': recent[0]['ja4s'],
'new_fingerprint': recent[-1]['ja4s'],
'change_time': recent[-1]['timestamp']
}
return {'changed': False}
# Usage
tracker = ServerFingerprintTracker()
# Track server over time
tracker.track('example.com', 't13h2h3c1301_original123')
# ... time passes ...
tracker.track('example.com', 't12h2htc02f_changed456')
result = tracker.detect_fingerprint_change('example.com')
if result['changed']:
print(f"ALERT: Server fingerprint changed!")
print(f"Server: example.com")
print(f"Old: {result['old_fingerprint']}")
print(f"New: {result['new_fingerprint']}")
print(f"This could indicate server compromise or infrastructure change!")Common JA4S Examples
Popular Web Servers
NGINX (Modern):
t13h2h3c1301_abc123def456
- TLS 1.3, h2+h3 ALPN, AES_128_GCM_SHA256Apache (Default):
t12h2htc02f_789abc012def
- TLS 1.2, h2+http/1.1, ECDHE_RSA_AES_128_GCMCloudflare:
t13h2h3c1302_cloudflare123
- TLS 1.3, h2+h3, AES_256_GCM_SHA384Malicious Servers
Cobalt Strike (Default):
t12000013301_cobaltstrike1
- TLS 1.2, No ALPN, AES_128_GCM_SHA256Metasploit (Default):
t12000010301_metasploit123
- TLS 1.2, No ALPN, RSA_AES_128_CBC_SHATroubleshooting
Issue 1: Fingerprint changes after server update
- Cause: Server configuration or TLS library update
- Solution: Maintain fingerprint versioning and update baselines
Issue 2: Same fingerprint across different servers
- Cause: Default configurations or shared infrastructure (CDN)
- Solution: Combine JA4S with JA4X (certificate fingerprinting)
Issue 3: Missing ALPN in older servers
- Cause: Server doesn't support ALPN extension
- Solution:
0000ALPN is valid, indicates older server
Best Practices
-
Baseline Legitimate Servers
- Document expected JA4S fingerprints for your services
- Update baselines after legitimate server changes
- Track fingerprint versions
-
Combine with Certificate Analysis
- Use JA4X in addition to JA4S
- Validate certificate chain
- Check certificate transparency logs
-
Threat Intelligence Integration
- Share malicious JA4S fingerprints with community
- Subscribe to threat intelligence feeds
- Automatically update detection rules
-
Monitor for Changes
- Alert on unexpected fingerprint changes
- Track infrastructure evolution
- Investigate rapid rotation
Key Takeaways
✅ JA4S fingerprints TLS servers by their Server Hello response ✅ Enables C2 detection and phishing infrastructure tracking ✅ Resistant to IP rotation and domain changes ✅ Combine with JA4 (client) and JA4X (certificate) for robust identification ✅ Essential for threat hunting and infrastructure mapping
Next Steps
- Practice: Capture and fingerprint various web servers
- Database: Build a collection of known-good server fingerprints
- Detection: Implement JA4S in your network monitoring
- Correlation: Combine JA4S with JA4 for full connection analysis
- Share: Contribute malicious fingerprints to the community
Related Techniques: