Defensive Go back to all


Hiding behind JA3 hash

- By Defensive Security


TLS fingerprint is a technique which bases on the specific set of information that is advertised in the "Hello" message. In practice, it's just 4th packet (but not necessarily) after 3-way handshake connection. However, it is sent by the client as the first message in the TLS handshake process.

It's unencrypted and can be easily inspected. In practice, every TLS (and for older versions SSL) client application uses a specific version of a particular implementation of SSL/TLS library. This fact is a kind of a main cause that set of advertised information can be common for different locations which use the same operating system, libraries, and applications. In short: thanks to this method we can determine that specific version of the application is used by a given client... and if it's on the blacklist (like malware application) can be just easily blocked.

(description from

JA3 gathers the decimal values of the bytes for the following fields in the client "Hello" packet:

  • SSL Version
  • Accepted Ciphers
  • List of Extensions
  • Elliptic Curves
  • and Elliptic Curve Formats.

It concatenates those values together in order, using a "," to delimit each field and a "-" to delimit each value in each field. Example:

  • 769,47-53-5-10-49161-49162-49171-49172-50-56-19-4,0-10-11,23-24-25,0

These strings are then MD5 hashed to produce an easily consumable and shareable 32 character fingerprint. Example:

  • 769,47-53-5-10-49161-49162-49171-49172-50-56-19-4,0-10-11,23-24-25,0 --> ada70206e40642a3e4461f35503241d5

According to some technical news:

  • "Akamai observed attackers using a technique dubbed: Cipher Stunting, or using advanced methods to randomize SSL/TLS signatures in an attempt to evade detection attempts".
  • "Over the last few months, attackers have been tampering with SSL/TLS signatures at a scale never before seen by Akamai"
  • "The TLS fingerprints that Akamai observed before Cipher Stunting was observed could be counted in the tens of thousands. Soon after the initial observation, that count ballooned to millions, and then recently jumped to billions."

It simply looks like an attacker can easily randomize client "Hello" message to mitigate or totally evade attempt.
But going further... what if one could use opposite method?! Instead of creating each time different hello message it could be possible to hide behind a fake fingerprint ?! Let's check it...



1. First, just ping server to determine IP address (it will be used later and for some traffic filters)

# ping ( 56(84) bytes of data.    

2. Use tcpdump to catch just 7 packets for specific IP address

# tcpdump host -w /tmp/curl.pcap -c 7    

3. Using curl just send a simple request to our google server (in my case:

# curl -k

4. Let's take a look at packets for curl.pcap from wireshark perspective:

# wireshark-gtk /tmp/curl.pcap   
  • JA3 method uses (for hash calculation) following fields:
  • (SSL)Version
  • Cipher(Suites)
  • (SSL)Extensions (including padding!)
  • Supported elliptic curve(s)
  • Elliptic curve point format

Now... using wireshark let's do some notes and copy needed bytes (in HEX format). 

In my case they have the following values:

  • version: 0x0301
  • cipher suites:
13 02 13 03 13 01 c0 2c c0 30 00 9f cc a9 cc a8
cc aa c0 2b c0 2f 00 9e c0 24 c0 28 00 6b c0 23
c0 27 00 67 c0 0a c0 14 00 39 c0 09 c0 13 00 33
00 9d 00 9c 00 3d 00 3c 00 35 00 2f 00 ff
  • after conversion to (2 bytes) hex format and adding comma:
0x1302, 0x1303, 0x1301, 0xC02C, 0xC030, 0x009F, 0xCCA9, 0xCCA8, 0xCCAA, 0xC02B, 0xC02F, 0x009E, 0xC024, 0xC028, 0x006B, 0xC023,
0xC027, 0x0067, 0xC00A,0xC014, 0x0039, 0xC009, 0xC013, 0x0033,0x009D, 0x009C, 0x003D, 0x003C, 0x0035, 0x002F, 0x00FF
  • extensions - let's note the name for each extension [and some byte values only for two of them (as a JA3 HASH part) [IT's VERY important to note and have the same order as we can see in wireshark extension list]
  • ec point formats (SupportedPoints): 0x00, 0x01, 0x02

  • supported groups (SupportedCurves): 0x001D, 0x0017, 0x001E, 0x0019, 0x0018

  • next protocol negotiation (NPN)
  • application layer protocol negotiation (ALPN)
  • encrypt then mac (EncThenMac)
  • extended master secret (ExtendedMasterSecret)
  • signature algorithms (SignatureAlgorithms)
  • supported versions (SupportedVersions)
  • psk key exchange modes (PSKKeyExchangeModes)
  • key share (KeyShare)
  • padding (UtlsPaddingExtension)

Having these values now we can use golang TLS library:

# git clone
# cd utls/examples
# go get
# go get
# go build . //[not needed but just check if golang compilation works...]

We have a few things to change in examples.go file:

  1. Add new function HttpGetCustomExfil with spec struct which is defined according notes from wireshark:
func HttpGetCustomExfil(hostname string, addr string) (*http.Response, error) {
	config := tls.Config{ServerName: hostname, InsecureSkipVerify: true}
	dialConn, err := net.DialTimeout("tcp", addr, dialTimeout)
	if err != nil {
		return nil, fmt.Errorf("net.DialTimeout error: %+v", err)
	uTlsConn := tls.UClient(dialConn, &config, tls.HelloCustom)
	defer uTlsConn.Close()

	// do not use this particular spec in production
	// make sure to generate a separate copy of ClientHelloSpec for every connection
	spec := tls.ClientHelloSpec{
		TLSVersMax: tls.VersionTLS13,
		TLSVersMin: tls.VersionTLS10,
		CipherSuites: []uint16{
			0x1302, 0x1303, 0x1301, 0xC02C, 0xC030, 0x009F, 0xCCA9, 0xCCA8, 0xCCAA, 0xC02B, 0xC02F, 0x009E, 0xC024, 0xC028, 0x006B, 0xC023, 0xC027, 0x0067, 0xC00A, 0xC014, 0x0039, 0xC009, 0xC013, 0x0033, 0x009D, 0x009C, 0x003D, 0x003C, 0x0035, 0x002F, 0x00FF,
		Extensions: []tls.TLSExtension{
			&tls.SupportedPointsExtension{SupportedPoints: []byte{0x00, 0x01, 0x02}},
			&tls.SupportedCurvesExtension{Curves: []tls.CurveID{0x001D, 0x0017, 0x001E, 0x0019, 0x0018 }},
			&tls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}},
			&tls.SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []tls.SignatureScheme{
				0x0403, 0x0503, 0x0603, 0x0807, 0x0808, 0x0809, 0x080A, 0x080B, 0x0804, 0x0805, 0x0806, 0x0401, 0x0501, 0x0601, 0x0303, 0x0203, 0x0301, 0x0201, 0x0302, 0x0202, 0x0402, 0x0502, 0x0602,
			&tls.PSKKeyExchangeModesExtension{[]uint8{1}}, // pskModeDHE
				{Group: tls.CurveID(tls.GREASE_PLACEHOLDER), Data: []byte{0}},
				{Group: tls.X25519},

			&tls.UtlsPaddingExtension{GetPaddingLen: tls.BoringPaddingStyle},
		GetSessionID: nil,
	err = uTlsConn.ApplyPreset(&spec)

	if err != nil {
		return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err)

	err = uTlsConn.Handshake()
	if err != nil {
		return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err)

	return httpGetOverConn(uTlsConn, uTlsConn.HandshakeState.ServerHello.AlpnProtocol)

2. Remove main function and copy-paste following:

func main() {
	var response *http.Response
	var err error

	response, err = HttpGetCustomExfil(requestHostname, requestAddr)
	if err != nil {
		fmt.Printf("#> HttpGetCustomExfil() failed: %+v\n", err)
	} else {
		fmt.Printf("#> HttpGetCustomExfil() response: %+s\n", dumpResponseNoBody(response))


3. Save file and compile:

# go build .

Using tcpdump catch just 7 packets for specific IP address //[you can use facebook address which is set by default or change it to google as it was above - it doesn't matter]

# tcpdump host -w /tmp/utls.pcap -c 7

Let's run compiled code:

# ./examples
HttpGetCustomExfil() response: HTTP/1.1 301 Moved Permanently
Connection: keep-alive
Content-Type: text/html; charset="utf-8"
Date: Fri, 27 Sep 2019 13:35:16 GMT
X-Fb-Debug: k4vdUCvbbj/vqhJjXugCF9XWaB2tXz2iH3E2cnL0yKf9HYHXLvUIYyeAGWKm22v0F/0nPdOkTNGewvndNV34zA==
Content-Length: 0

4. And final part is related directly with script usage because having pcap files for CURL connection and UTLS connection we can calculate and compare both (JA3) hashes:

# git clone
# sudo pip install dpkt
# cd ja3/python
./ /tmp/curl.pcap
[] JA3: 
11-10-13172-16-22-23-13-43-45-51-21,29-23-30-25-24,0-1-2 --> 732b9e6543be52016be7e6ac897d24d4

./ /tmp/utls.pcap
[]   JA3: 
11-10-13172-16-22-23-13-43-45-51-21,29-23-30-25-24,0-1-2 --> 732b9e6543be52016be7e6ac897d24d4


As you can see both HASHes are the same! Even having totally custom application with own code it is possible to imitate TLS connection which for fingerprint function will look like a common unsuspected and valid CURL (as in this example) hash. Still, keep in mind that JA3 is just a simple technique and can be easily mitigated!



the tool and the approach is a part of 'In & Out - Network Exfiltration Techniques' Training class, that I am delivering during the upcoming:

Cyber Week in Abu Dhabi - The Biggest Hack In The Box Event Of The Year