Added 'JA4Server Fingerprint' operation
This commit is contained in:
parent
d13218caaf
commit
7b2d572902
@ -238,6 +238,7 @@
|
||||
"JA3 Fingerprint",
|
||||
"JA3S Fingerprint",
|
||||
"JA4 Fingerprint",
|
||||
"JA4Server Fingerprint",
|
||||
"HASSH Client Fingerprint",
|
||||
"HASSH Server Fingerprint",
|
||||
"Format MAC addresses",
|
||||
|
@ -25,6 +25,9 @@ export function toJA4(bytes) {
|
||||
let tlsr = {};
|
||||
try {
|
||||
tlsr = parseTLSRecord(bytes);
|
||||
if (tlsr.handshake.value.handshakeType.value !== 0x01) {
|
||||
throw new Error();
|
||||
}
|
||||
} catch (err) {
|
||||
throw new OperationError("Data is not a valid TLS Client Hello. QUIC is not yet supported.\n" + err);
|
||||
}
|
||||
@ -48,16 +51,7 @@ export function toJA4(bytes) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
switch (version) {
|
||||
case 0x0304: version = "13"; break; // TLS 1.3
|
||||
case 0x0303: version = "12"; break; // TLS 1.2
|
||||
case 0x0302: version = "11"; break; // TLS 1.1
|
||||
case 0x0301: version = "10"; break; // TLS 1.0
|
||||
case 0x0300: version = "s3"; break; // SSL 3.0
|
||||
case 0x0200: version = "s2"; break; // SSL 2.0
|
||||
case 0x0100: version = "s1"; break; // SSL 1.0
|
||||
default: version = "00"; // Unknown
|
||||
}
|
||||
version = tlsVersionMapper(version);
|
||||
|
||||
/* SNI
|
||||
If the SNI extension (0x0000) exists, then the destination of the connection is a domain, or “d” in the fingerprint.
|
||||
@ -99,6 +93,7 @@ export function toJA4(bytes) {
|
||||
if (ext.type.value === "application_layer_protocol_negotiation") {
|
||||
alpn = parseFirstALPNValue(ext.value.data);
|
||||
alpn = alpn.charAt(0) + alpn.charAt(alpn.length - 1);
|
||||
if (alpn.charCodeAt(0) > 127) alpn = "99";
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -164,3 +159,106 @@ export function toJA4(bytes) {
|
||||
"JA4_ro": `${ptype}${version}${sni}${cipherLen}${extLen}${alpn}_${originalCiphersRaw}_${originalExtensionsRaw}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the JA4Server from a given TLS Server Hello Stream
|
||||
* @param {Uint8Array} bytes
|
||||
* @returns {string}
|
||||
*/
|
||||
export function toJA4S(bytes) {
|
||||
let tlsr = {};
|
||||
try {
|
||||
tlsr = parseTLSRecord(bytes);
|
||||
if (tlsr.handshake.value.handshakeType.value !== 0x02) {
|
||||
throw new Error();
|
||||
}
|
||||
} catch (err) {
|
||||
throw new OperationError("Data is not a valid TLS Server Hello. QUIC is not yet supported.\n" + err);
|
||||
}
|
||||
|
||||
/* QUIC
|
||||
“q” or “t”, which denotes whether the hello packet is for QUIC or TCP.
|
||||
TODO: Implement QUIC
|
||||
*/
|
||||
const ptype = "t";
|
||||
|
||||
/* TLS Version
|
||||
TLS version is shown in 3 different places. If extension 0x002b exists (supported_versions), then the version
|
||||
is the highest value in the extension. Remember to ignore GREASE values. If the extension doesn’t exist, then
|
||||
the TLS version is the value of the Protocol Version. Handshake version (located at the top of the packet)
|
||||
should be ignored.
|
||||
*/
|
||||
let version = tlsr.version.value;
|
||||
for (const ext of tlsr.handshake.value.extensions.value) {
|
||||
if (ext.type.value === "supported_versions") {
|
||||
version = parseHighestSupportedVersion(ext.value.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
version = tlsVersionMapper(version);
|
||||
|
||||
/* Number of Extensions
|
||||
2 character number of cipher suites, so if there’s 6 cipher suites in the hello packet, then the value should be “06”.
|
||||
If there’s > 99, which there should never be, then output “99”.
|
||||
*/
|
||||
let extLen = tlsr.handshake.value.extensions.value.length;
|
||||
extLen = extLen > 99 ? "99" : extLen.toString().padStart(2, "0");
|
||||
|
||||
/* ALPN Extension Chosen Value
|
||||
The first and last characters of the ALPN (Application-Layer Protocol Negotiation) first value.
|
||||
If there are no ALPN values or no ALPN extension then we print “00” as the value in the fingerprint.
|
||||
*/
|
||||
let alpn = "00";
|
||||
for (const ext of tlsr.handshake.value.extensions.value) {
|
||||
if (ext.type.value === "application_layer_protocol_negotiation") {
|
||||
alpn = parseFirstALPNValue(ext.value.data);
|
||||
alpn = alpn.charAt(0) + alpn.charAt(alpn.length - 1);
|
||||
if (alpn.charCodeAt(0) > 127) alpn = "99";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Chosen Cipher
|
||||
The hex value of the chosen cipher suite
|
||||
*/
|
||||
const cipher = toHexFast(tlsr.handshake.value.cipherSuite.data);
|
||||
|
||||
/* Extension hash
|
||||
A 12 character truncated sha256 hash of the list of extensions.
|
||||
The extension list is created using the 4 character hex values of the extensions, lower case, comma delimited.
|
||||
*/
|
||||
const extensionsList = [];
|
||||
for (const ext of tlsr.handshake.value.extensions.value) {
|
||||
extensionsList.push(toHexFast(ext.type.data));
|
||||
}
|
||||
const extensionsRaw = extensionsList.join(",");
|
||||
const extensionsHash = runHash(
|
||||
"sha256",
|
||||
Utils.strToArrayBuffer(extensionsRaw)
|
||||
).substring(0, 12);
|
||||
|
||||
return {
|
||||
"JA4S": `${ptype}${version}${extLen}${alpn}_${cipher}_${extensionsHash}`,
|
||||
"JA4S_r": `${ptype}${version}${extLen}${alpn}_${cipher}_${extensionsRaw}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Takes a TLS version value and returns a JA4 TLS version string
|
||||
* @param {Uint8Array} version - Two byte array of version number
|
||||
* @returns {string}
|
||||
*/
|
||||
function tlsVersionMapper(version) {
|
||||
switch (version) {
|
||||
case 0x0304: return "13"; // TLS 1.3
|
||||
case 0x0303: return "12"; // TLS 1.2
|
||||
case 0x0302: return "11"; // TLS 1.1
|
||||
case 0x0301: return "10"; // TLS 1.0
|
||||
case 0x0300: return "s3"; // SSL 3.0
|
||||
case 0x0200: return "s2"; // SSL 2.0
|
||||
case 0x0100: return "s1"; // SSL 1.0
|
||||
default: return "00"; // Unknown
|
||||
}
|
||||
}
|
||||
|
@ -70,13 +70,11 @@ function parseHandshake(bytes) {
|
||||
|
||||
// Handshake type
|
||||
h.handshakeType = {
|
||||
description: "Client Hello",
|
||||
description: "Handshake Type",
|
||||
length: 1,
|
||||
data: b.getBytes(1),
|
||||
value: s.readInt(1)
|
||||
};
|
||||
if (h.handshakeType.value !== 0x01)
|
||||
throw new OperationError("Not a Client Hello.");
|
||||
|
||||
// Handshake length
|
||||
h.handshakeLength = {
|
||||
@ -86,8 +84,33 @@ function parseHandshake(bytes) {
|
||||
value: s.readInt(3)
|
||||
};
|
||||
if (s.length !== h.handshakeLength.value + 4)
|
||||
throw new OperationError("Not enough data in Client Hello.");
|
||||
throw new OperationError("Not enough data in Handshake message.");
|
||||
|
||||
|
||||
switch (h.handshakeType.value) {
|
||||
case 0x01:
|
||||
h.handshakeType.description = "Client Hello";
|
||||
parseClientHello(s, b, h);
|
||||
break;
|
||||
case 0x02:
|
||||
h.handshakeType.description = "Server Hello";
|
||||
parseServerHello(s, b, h);
|
||||
break;
|
||||
default:
|
||||
throw new OperationError("Not a known handshake message.");
|
||||
}
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a TLS Client Hello
|
||||
* @param {Stream} s
|
||||
* @param {Stream} b
|
||||
* @param {Object} h
|
||||
* @returns {JSON}
|
||||
*/
|
||||
function parseClientHello(s, b, h) {
|
||||
// Hello version
|
||||
h.helloVersion = {
|
||||
description: "Client Hello Version",
|
||||
@ -171,6 +194,79 @@ function parseHandshake(bytes) {
|
||||
return h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a TLS Server Hello
|
||||
* @param {Stream} s
|
||||
* @param {Stream} b
|
||||
* @param {Object} h
|
||||
* @returns {JSON}
|
||||
*/
|
||||
function parseServerHello(s, b, h) {
|
||||
// Hello version
|
||||
h.helloVersion = {
|
||||
description: "Server Hello Version",
|
||||
length: 2,
|
||||
data: b.getBytes(2),
|
||||
value: s.readInt(2)
|
||||
};
|
||||
|
||||
// Random
|
||||
h.random = {
|
||||
description: "Server Random",
|
||||
length: 32,
|
||||
data: b.getBytes(32),
|
||||
value: s.getBytes(32)
|
||||
};
|
||||
|
||||
// Session ID Length
|
||||
h.sessionIDLength = {
|
||||
description: "Session ID Length",
|
||||
length: 1,
|
||||
data: b.getBytes(1),
|
||||
value: s.readInt(1)
|
||||
};
|
||||
|
||||
// Session ID
|
||||
h.sessionID = {
|
||||
description: "Session ID",
|
||||
length: h.sessionIDLength.value,
|
||||
data: b.getBytes(h.sessionIDLength.value),
|
||||
value: s.getBytes(h.sessionIDLength.value)
|
||||
};
|
||||
|
||||
// Cipher Suite
|
||||
h.cipherSuite = {
|
||||
description: "Selected Cipher Suite",
|
||||
length: 2,
|
||||
data: b.getBytes(2),
|
||||
value: CIPHER_SUITES_LOOKUP[s.readInt(2)] || "Unknown"
|
||||
};
|
||||
|
||||
// Compression Method
|
||||
h.compressionMethod = {
|
||||
description: "Selected Compression Method",
|
||||
length: 1,
|
||||
data: b.getBytes(1),
|
||||
value: s.readInt(1) // TODO: Compression method name here
|
||||
};
|
||||
|
||||
// Extensions Length
|
||||
h.extensionsLength = {
|
||||
description: "Extensions Length",
|
||||
length: 2,
|
||||
data: b.getBytes(2),
|
||||
value: s.readInt(2)
|
||||
};
|
||||
|
||||
// Extensions
|
||||
h.extensions = {
|
||||
description: "Extensions",
|
||||
length: h.extensionsLength.value,
|
||||
data: b.getBytes(h.extensionsLength.value),
|
||||
value: parseExtensions(s.getBytes(h.extensionsLength.value))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Cipher Suites
|
||||
* @param {Uint8Array} bytes
|
||||
@ -748,6 +844,11 @@ export const GREASE_VALUES = [
|
||||
export function parseHighestSupportedVersion(bytes) {
|
||||
const s = new Stream(bytes);
|
||||
|
||||
// The Server Hello supported_versions extension simply contains the chosen version
|
||||
if (s.length === 2) {
|
||||
return s.readInt(2);
|
||||
}
|
||||
|
||||
// Length
|
||||
let i = s.readInt(1);
|
||||
|
||||
|
66
src/core/operations/JA4ServerFingerprint.mjs
Normal file
66
src/core/operations/JA4ServerFingerprint.mjs
Normal file
@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2024
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation.mjs";
|
||||
import Utils from "../Utils.mjs";
|
||||
import {toJA4S} from "../lib/JA4.mjs";
|
||||
|
||||
/**
|
||||
* JA4Server Fingerprint operation
|
||||
*/
|
||||
class JA4ServerFingerprint extends Operation {
|
||||
|
||||
/**
|
||||
* JA4ServerFingerprint constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "JA4Server Fingerprint";
|
||||
this.module = "Crypto";
|
||||
this.description = "Generates a JA4Server Fingerprint (JA4S) to help identify TLS servers or sessions based on hashing together values from the Server Hello.<br><br>Input: A hex stream of the TLS or QUIC Server Hello packet application layer.";
|
||||
this.infoURL = "https://medium.com/foxio/ja4-network-fingerprinting-9376fe9ca637";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
{
|
||||
name: "Input format",
|
||||
type: "option",
|
||||
value: ["Hex", "Base64", "Raw"]
|
||||
},
|
||||
{
|
||||
name: "Output format",
|
||||
type: "option",
|
||||
value: ["JA4S", "JA4S Raw", "Both"]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
const [inputFormat, outputFormat] = args;
|
||||
input = Utils.convertToByteArray(input, inputFormat);
|
||||
const ja4s = toJA4S(new Uint8Array(input));
|
||||
|
||||
// Output
|
||||
switch (outputFormat) {
|
||||
case "JA4S":
|
||||
return ja4s.JA4S;
|
||||
case "JA4S Raw":
|
||||
return ja4s.JA4S_r;
|
||||
case "Both":
|
||||
default:
|
||||
return `JA4S: ${ja4s.JA4S}\nJA4S_r: ${ja4s.JA4S_r}`;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default JA4ServerFingerprint;
|
@ -83,7 +83,7 @@ import "./tests/HKDF.mjs";
|
||||
import "./tests/Image.mjs";
|
||||
import "./tests/IndexOfCoincidence.mjs";
|
||||
import "./tests/JA3Fingerprint.mjs";
|
||||
import "./tests/JA4Fingerprint.mjs";
|
||||
import "./tests/JA4.mjs";
|
||||
import "./tests/JA3SFingerprint.mjs";
|
||||
import "./tests/JSONBeautify.mjs";
|
||||
import "./tests/JSONMinify.mjs";
|
||||
|
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* JA4Fingerprint tests.
|
||||
* JA4 tests.
|
||||
*
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2024
|
||||
@ -52,4 +52,70 @@ TestRegister.addTests([
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "JA4Server Fingerprint: TLS 1.2 h2 ALPN",
|
||||
input: "16030300640200006003035f0236c07f47bfb12dc2da706ecb3fe7f9eeac9968cc2ddf444f574e4752440120b89ff1ab695278c69b8a73f76242ef755e0b13dc6d459aaaa784fec9c2dfce34cca900001800000000ff01000100000b00020100001000050003026832",
|
||||
expectedOutput: "t1204h2_cca9_1428ce7b4018",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "JA4Server Fingerprint",
|
||||
"args": ["Hex", "JA4S"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "JA4Server Fingerprint: TLS 1.2 h2 ALPN Raw",
|
||||
input: "16030300640200006003035f0236c07f47bfb12dc2da706ecb3fe7f9eeac9968cc2ddf444f574e4752440120b89ff1ab695278c69b8a73f76242ef755e0b13dc6d459aaaa784fec9c2dfce34cca900001800000000ff01000100000b00020100001000050003026832",
|
||||
expectedOutput: "t1204h2_cca9_0000,ff01,000b,0010",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "JA4Server Fingerprint",
|
||||
"args": ["Hex", "JA4S Raw"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "JA4Server Fingerprint: TLS 1.3",
|
||||
input: "160303007a020000760303236d214556452c55a0754487e64b1a8b0262c50ba23004c9d504166a6de3439920d0b0099243c9296a0c84153ea4ada7d87ad017f4211c2ea1350b0b3cc5514d5f130100002e00330024001d002099e3cc43a2c9941ae75af1b2c7a629bee3ee7031973cad85c82f2f23677fb244002b00020304",
|
||||
expectedOutput: "t130200_1301_234ea6891581",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "JA4Server Fingerprint",
|
||||
"args": ["Hex", "JA4S"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "JA4Server Fingerprint: TLS 1.3 Raw",
|
||||
input: "160303007a020000760303236d214556452c55a0754487e64b1a8b0262c50ba23004c9d504166a6de3439920d0b0099243c9296a0c84153ea4ada7d87ad017f4211c2ea1350b0b3cc5514d5f130100002e00330024001d002099e3cc43a2c9941ae75af1b2c7a629bee3ee7031973cad85c82f2f23677fb244002b00020304",
|
||||
expectedOutput: "t130200_1301_0033,002b",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "JA4Server Fingerprint",
|
||||
"args": ["Hex", "JA4S Raw"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "JA4Server Fingerprint: TLS 1.3 non-ascii ALPN",
|
||||
input: "160303007a020000760303897c232e3ee313314f2b662307ff4f7e2cf1caeec1b27711bca77f469519168520bc58b92f865e6b9aa4a6371cadcb0afe1da1c0f705209a11d52357f56d5dd962130100002e00330024001d002076b8b7ed0f96b63a773d85ab6f3a87a151c130529785b41a4defb53184055957002b00020304",
|
||||
expectedOutput: "t130200_1301_234ea6891581",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "JA4Server Fingerprint",
|
||||
"args": ["Hex", "JA4S"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "JA4Server Fingerprint: TLS 1.3 non-ascii ALPN Raw",
|
||||
input: "160303007a020000760303897c232e3ee313314f2b662307ff4f7e2cf1caeec1b27711bca77f469519168520bc58b92f865e6b9aa4a6371cadcb0afe1da1c0f705209a11d52357f56d5dd962130100002e00330024001d002076b8b7ed0f96b63a773d85ab6f3a87a151c130529785b41a4defb53184055957002b00020304",
|
||||
expectedOutput: "t130200_1301_0033,002b",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "JA4Server Fingerprint",
|
||||
"args": ["Hex", "JA4S Raw"]
|
||||
}
|
||||
]
|
||||
},
|
||||
]);
|
Loading…
Reference in New Issue
Block a user