Quick-Labs
JA4
JA4S - TLS Server Fingerprinting

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

  1. C2 Server Detection: Identify command-and-control infrastructure
  2. Phishing Detection: Detect fake login pages and credential harvesters
  3. Server Impersonation: Identify servers masquerading as legitimate services
  4. Infrastructure Mapping: Map attacker infrastructure across IP changes
  5. 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 TCP
  • q = QUIC
  • d = DTLS over UDP

2. TLS Version (2 chars)

Server-selected TLS version:

  • 13 = TLS 1.3
  • 12 = TLS 1.2
  • 11 = TLS 1.1
  • 10 = TLS 1.0
  • s3 = SSL 3.0
  • d2 = DTLS 1.2
  • d3 = DTLS 1.3

3. ALPN (2 chars each)

First 2 characters of first two ALPN values:

  • h2 = HTTP/2
  • h3 = HTTP/3 (QUIC)
  • ht = http/1.1
  • 00 = 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_SHA256
  • 1302 = TLS_AES_256_GCM_SHA384
  • c02f = 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.1

Step 1: Extract Protocol and Version

  • Protocol: TLS over TCP → t
  • TLS Version: 0x0303 (TLS 1.2) → 12

Step 2: Extract ALPN Values

  • First ALPN: h2h2
  • Second ALPN: http/1.1ht (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, ff01

Python 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: 23ee972fd6b4

Step 5: Assemble the Fingerprint

t12h2htc02f_23ee972fd6b4

Breakdown:

  • t = TLS over TCP
  • 12 = TLS 1.2
  • h2ht = ALPN: h2 and http/1.1
  • c02f = Chosen cipher
  • 23ee972fd6b4 = 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

  1. Start Capture: Select network interface
  2. Filter: tls.handshake.type == 2 (Server Hello)
  3. Analyze: Expand TLS → Handshake Protocol → Server Hello
  4. 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 -X

Using 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_name

Advanced 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

NGINX (Modern):

t13h2h3c1301_abc123def456
- TLS 1.3, h2+h3 ALPN, AES_128_GCM_SHA256

Apache (Default):

t12h2htc02f_789abc012def
- TLS 1.2, h2+http/1.1, ECDHE_RSA_AES_128_GCM

Cloudflare:

t13h2h3c1302_cloudflare123
- TLS 1.3, h2+h3, AES_256_GCM_SHA384

Malicious Servers

Cobalt Strike (Default):

t12000013301_cobaltstrike1
- TLS 1.2, No ALPN, AES_128_GCM_SHA256

Metasploit (Default):

t12000010301_metasploit123
- TLS 1.2, No ALPN, RSA_AES_128_CBC_SHA

Troubleshooting

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: 0000 ALPN is valid, indicates older server

Best Practices

  1. Baseline Legitimate Servers

    • Document expected JA4S fingerprints for your services
    • Update baselines after legitimate server changes
    • Track fingerprint versions
  2. Combine with Certificate Analysis

    • Use JA4X in addition to JA4S
    • Validate certificate chain
    • Check certificate transparency logs
  3. Threat Intelligence Integration

    • Share malicious JA4S fingerprints with community
    • Subscribe to threat intelligence feeds
    • Automatically update detection rules
  4. 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

  1. Practice: Capture and fingerprint various web servers
  2. Database: Build a collection of known-good server fingerprints
  3. Detection: Implement JA4S in your network monitoring
  4. Correlation: Combine JA4S with JA4 for full connection analysis
  5. Share: Contribute malicious fingerprints to the community

Related Techniques: