diff --git a/Access/README.md b/Access/README.md new file mode 100644 index 0000000..3918935 --- /dev/null +++ b/Access/README.md @@ -0,0 +1,42 @@ +# F5 iRules for JA4+ Network Fingerprinting within Access flow + +F5 iRules for generating JA4+ fingerprints. Currently, only JA4, JA4S, JA4T, JA4L, and JA4H fingerprint iRules are provided. More JA4+ fingerprint iRules *MAY* be added in the future. +The using those fingerprints within Access flow whether to restrict or query ja4db for matching items. + +> [!WARNING] +>DISCLAIMER: These iRules are provided as-is with no guarantee of performance or functionality. Use at your own risk. +>These iRules have been tested on F5 BIGIPs running TMOS versions 16.1 and 17.1. + + +## What is JA4+ Network Fingerprinting? + +From the [FoxIO JA4+ Repo](https://github.com/FoxIO-LLC/ja4): +>JA4+ is a suite of network fingerprinting methods that are easy to use and easy to share. These methods are both human >and machine readable to facilitate more effective threat-hunting and analysis. The use-cases for these fingerprints >include scanning for threat actors, malware detection, session hijacking prevention, compliance automation, location >tracking, DDoS detection, grouping of threat actors, reverse shell detection, and many more. + +Please read this blog post for more details: [JA4+ Network Fingerprinting](https://medium.com/foxio/ja4-network-fingerprinting-9376fe9ca637) + +To understand how to read JA4+ fingerprints, see [Technical Details](https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/README.md) + +## JA4+ Licensing + +> [!IMPORTANT] +>**JA4 TLS Client Fingerprinting is licensed under BSD 3-Clause** +> +>_Copyright (c) 2024, FoxIO_ +>_All rights reserved. +>JA4 TLS Client Fingerprinting is Open-Source, Licensed under BSD 3-Clause. +>For full license text and more details, see the repo root https://github.com/FoxIO-LLC/ja4_ +> +> +>**All other JA4+ Fingerprints are under the FoxIO License 1.1** +> +>_Copyright (c) 2024, FoxIO, LLC. +>All rights reserved. +>Licensed under FoxIO License 1.1 +>For full license text and more details, see the repo root https://github.com/FoxIO-LLC/ja4_ + +## How to Use + +> Copy the iRules from this folder. +> Add iRule event trigger in your Access Policy, take care of the trigger ID in the iRule. +> In case you try to query external JA4db website, you can utilize HTTP connector to make use of the obtained parameters. diff --git a/Access/ja4-access.irule b/Access/ja4-access.irule new file mode 100644 index 0000000..66e8938 --- /dev/null +++ b/Access/ja4-access.irule @@ -0,0 +1,244 @@ +############################################################################################### +# iRule to calculate JA4 "Client TLS" save in Access variable +# See JA4 spec on GitHub for more details +# https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md +# +# Copyright (c) 2024, FoxIO +# All rights reserved. +# JA4 TLS Client Fingerprinting is Open-Source, Licensed under BSD 3-Clause +# For full license text and more details, see the repo root https://github.com/FoxIO-LLC/ja4 +############################################################################################### + +proc parseClientHello { payload rlen ja4_ver ja4_tprt } { + + set ja4_sni "i" + ## Define GREASE values so these can be excluded from cipher list + set greaseList "0a0a 1a1a 2a2a 3a3a 4a4a 5a5a 6a6a 7a7a 8a8a 9a9a aaaa baba caca dada eaea fafa" + + ## HEADERS - SKIP: Already captured in XXX_DATA event (record header, handshake header, server version, server random) + set field_offset 43 + + ## SESSION ID - SKIP + binary scan ${payload} @${field_offset}c sessID_len + set field_offset [expr {${field_offset} + 1 + ${sessID_len}}] + + ## CLIENT CIPHERS + ## Capture cipher list length and incr offset + binary scan ${payload} @${field_offset}S cipherList_len + set field_offset [expr {${field_offset} + 2}] + + set cipher_offset 0 + set cipher_cnt 0 + set cipher_list [list] + while { [expr {${cipher_offset} < ${cipherList_len}}] } { + binary scan ${payload} @${field_offset}H4 cipher_hex + if { [lsearch -sorted -inline $greaseList $cipher_hex] eq "" } { + lappend cipher_list ${cipher_hex} + incr cipher_cnt + } + set cipher_offset [expr {${cipher_offset} + 2}] + set field_offset [expr {${field_offset} + 2}] + } + ## Sort cipher_list + set cipher_list [lsort $cipher_list] + ## Convert list to comma-separated string + set cipher_str "" + foreach cipher_hex $cipher_list { + append cipher_str "${cipher_hex}," + } + set cipher_str [string trimright ${cipher_str} ","] + ## Get truncated hash of cipher list string + binary scan [sha256 ${cipher_str}] H* cipher_hash + set trunc_cipher_hash [string range $cipher_hash 0 11] + + ## Format cipher count + if { $cipher_cnt > 99 } { + set cipher_cnt 99 + } + set ja4_ccnt [format "%02d" $cipher_cnt] + + ## COMPRESSION METHOD - SKIP + binary scan ${payload} @${field_offset}c compression_len + set field_offset [expr {${field_offset} + 1 + ${compression_len}}] + + + ## EXTENSIONS + set ja4_ecnt 0 + set ja4_etype_list [list] + set ja4_alpn "00" + set siga_list "" + + ## Check if there is more data + if { [expr {${field_offset} < ${rlen}}] } { + ## Capture Extensions length and incr offset + binary scan ${payload} @${field_offset}S extList_len + set field_offset [expr {${field_offset} + 2}] + + ## Pad rlen by 1 byte + set rlen [expr ${rlen} + 1] + + ## Parse Extensions + while { [expr {${field_offset} <= ${rlen}}] } { + ## Capture Ext Type, Incr offset past Ext Type + binary scan ${payload} @${field_offset}H4 ext_hex + set field_offset [expr {${field_offset} + 2}] + + ## Capture Ext Length, Incr offset past Ext Length + binary scan ${payload} @${field_offset}S ext_len + set field_offset [expr {${field_offset} + 2}] + + ## Check for GREASE values, if GREASE incr offset by Ext Length + if {[lsearch -sorted -inline $greaseList $ext_hex] ne "" } { + set field_offset [expr {${field_offset} + ${ext_len}}] + continue + } else { + ## Check for specific Extension Types + switch $ext_hex { + "0000" { + ## SNI (00) + ## Set JA4 domain/ip field + set ja4_sni "d" + incr ja4_ecnt + } + "000d" { + ## Signature Algorithms (13) + ## Capture Signature Algorithms length + binary scan ${payload} @${field_offset}S siga_len + set siga_offset 0 + while { [expr {${siga_offset} <= ${siga_len}}] } { + binary scan ${payload} @[expr {${field_offset} + 2 + ${siga_offset}}]H4 siga_hex + if { [lsearch -sorted -inline $greaseList $siga_hex] eq "" } { + append siga_list "${siga_hex}," + } + incr siga_offset 2 + } + set siga_list [string trimright ${siga_list} ","] + lappend ja4_etype_list ${ext_hex} + incr ja4_ecnt + + } + "0010" { + ## ALPN (16) + ## Capture APLN length and First ALPN string length + binary scan ${payload} @${field_offset}Sc alpn_len alpn_str_len + ## Capture the First APLN string value + binary scan ${payload} @[expr {${field_offset} + 3}]a${alpn_str_len} alpn_str + incr ja4_ecnt + } + "0027" { + ## Supported EKT Ciphers (39) + ## Set JA4 Transport Protocol as QUIC + set ja4_tprt "q" + lappend ja4_etype_list ${ext_hex} + incr ja4_ecnt + } + "002b" { + ## Supported Versions (43) + ## Capture Supported Versions length + binary scan ${payload} @${field_offset}c sver_len + set sver_offset 0 + set sver_list [list] + while { [expr {${sver_offset} < ${sver_len}}] } { + binary scan ${payload} @[expr {${field_offset} + 1 + ${sver_offset}}]H4 sver_hex + if { [lsearch -sorted -inline $greaseList $sver_hex] eq "" } { + lappend sver_list ${sver_hex} + } + incr sver_offset 2 + } + set sver_list [lsort $sver_list] + set ja4_ver [lindex $sver_list end] + lappend ja4_etype_list ${ext_hex} + incr ja4_ecnt + } default { + lappend ja4_etype_list ${ext_hex} + incr ja4_ecnt + } + } + + ## Incr offset past the extension data length. Repeat this loop until we reach rlen (the end of the payload) + set field_offset [expr {${field_offset} + ${ext_len}}] + } + } + } + ## Set JA4 ALPN value + if { [info exist alpn_str] } { + set ja4_alpn "[string index ${alpn_str} 0][string index ${alpn_str} end]" + } + + ## Format extensions count var + if { $ja4_ecnt > 99 } { + set ja4_ecnt 99 + } + set ja4_ecnt [format "%02d" $ja4_ecnt] + + ## Sort and format extensions type list + set ja4_etype_list [lsort $ja4_etype_list] + set ja4_etype_str "" + foreach ext_type_hex $ja4_etype_list { + append ja4_etype_str "${ext_type_hex}," + } + set ja4_etype_str [string trimright ${ja4_etype_str} ","] + ## If present, append signature algorithms list to extensions list + if { ${siga_list} ne ""} { + set ja4_etype_str "${ja4_etype_str}_${siga_list}" + } + ## Hash extensions list + binary scan [sha256 ${ja4_etype_str}] H* ja4_ext_hash + set ja4_ext_hash_trunc [string range ${ja4_ext_hash} 0 11] + + ## Format version + switch $ja4_ver { + 0304 { set ja4_ver "13" } + 0303 { set ja4_ver "12" } + 0302 { set ja4_ver "11" } + 0301 { set ja4_ver "10" } + 0300 { set ja4_ver "s3" } + 0200 { set ja4_ver "s2" } + 0100 { set ja4_ver "s1" } + } + + ##Build JA4 string + set ja4_str "${ja4_tprt}${ja4_ver}${ja4_sni}${ja4_ccnt}${ja4_ecnt}${ja4_alpn}_${trunc_cipher_hash}_${ja4_ext_hash_trunc}" + set ja4_r_str "${ja4_tprt}${ja4_ver}${ja4_sni}${ja4_ccnt}${ja4_ecnt}${ja4_alpn}_${cipher_str}_${ja4_etype_str}" + + return "${ja4_str}" +} + + +when CLIENT_ACCEPTED { + unset -nocomplain rlen + set ja4_tprt "t" + ## Collect the TCP payload + TCP::collect +} + +when CLIENT_DATA { + + ## Get the TLS packet type and versions + if { ! [info exists rlen] } { + binary scan [TCP::payload] cH4ScH6H4 rtype proto_ver rlen hs_type rilen server_ver + #log local0. "rtype ${rtype} proto_ver ${proto_ver} rlen ${rlen} hs_type ${hs_type} rilen ${rilen} server_ver ${server_ver}" + + if { ( ${rtype} == 22 ) and ( ${hs_type} == 1 ) } { + #log local0. "Found CLIENT_HELLO" + set ja4 [call parseClientHello [TCP::payload] ${rlen} ${server_ver} ${ja4_tprt}] + #log local0. "JA4: '${ja4}'" + + } + } + + # Collect the rest of the record if necessary + if { [TCP::payload length] < $rlen } { + TCP::collect $rlen + } + + ## Release the payload + TCP::release +} + +when ACCESS_POLICY_AGENT_EVENT { + if { [ACCESS::policy agent_id] eq "JA4FP" } { + ACCESS::session data set session.custom.JA4 $ja4 + + } +} diff --git a/Access/ja4h-access.irule b/Access/ja4h-access.irule new file mode 100644 index 0000000..a0d2fd9 --- /dev/null +++ b/Access/ja4h-access.irule @@ -0,0 +1,110 @@ +################################################################################################################################### +# iRule to calculate JA4H "HTTP Request" done after Access policy is evaluated due to the need for HTTP request event to be fired. +# See JA4H spec on GitHub for more details +# https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4H.md +# +# Copyright (c) 2024, FoxIO, LLC. +# All rights reserved. +# Licensed under FoxIO License 1.1 +# For full license text and more details, see the repo root https://github.com/FoxIO-LLC/ja4 +###################################################################################################################################### + +when CLIENT_ACCEPTED { + # Use these variables to define the -r (raw) and -o (original) switches defined in the JA4 spec + # 0 = false/disabled (default) and 1 = true/enabled + set ja4h_raw 1 + set ja4h_original 0 + +} + +when HTTP_REQUEST priority 10 { + #Collect JA4H "a" values + set me [string range [string tolower [HTTP::method]] 0 1] + set v [string map {"." ""} [HTTP::version]] + set c "n" + set r "n" + if { [HTTP::header exists "cookie"] } { + set c "c" + } + if { [HTTP::header exists "referer"] } { + set r "r" + } + set lang "0000" + if { [set alval [HTTP::header value "accept-language"]] ne "" } { + if { $alval contains ";" } { + set alval [string range $alval 0 [string first ";" $alval]] + } + set alval [string tolower [string range [string map {"-" ""} ${alval}] 0 3]] + set lang [string replace $lang 0 [string length ${alval}] ${alval}] + } + + #Collect JA4H "b" values + set hc 0 + set hstr "" + foreach hname [HTTP::header names] { + if { ${hname} starts_with "X-JA4" } { + continue + } elseif { ([string tolower ${hname}] eq "cookie") || ([string tolower ${hname}] eq "referer")} { + if { ${ja4h_original} }{ + append hstr "${hname}," + } + } else { + incr hc + append hstr "${hname}," + } + } + + if { $hc > 99 } { + set hc 99 + } + set hc [format "%02d" $hc] + set hstr [string trimright ${hstr} ","] + binary scan [sha256 ${hstr}] H* hhash + set trunc_hhash [string range $hhash 0 11] + + #Collect JA4H "c" and "d" values + set cstr "" + set clist [list] + set ckvstr "" + #set ckvlist [list] + foreach cname [HTTP::cookie names] { + lappend clist ${cname} + } + if {${ja4h_original} == 0 }{ + set clist [lsort ${clist}] + } + foreach ck ${clist} { + append cstr "${ck}," + append ckvstr "${ck}=[HTTP::cookie value ${ck}]," + } + + set cstr [string trimright ${cstr} ","] + set ckvstr [string trimright ${ckvstr} ","] + if { $c eq "c" } { + binary scan [sha256 ${cstr}] H* chash + binary scan [sha256 ${ckvstr}] H* ckvhash + set trunc_chash [string range $chash 0 11] + set trunc_ckvhash [string range $ckvhash 0 11] + } else { + set trunc_chash "000000000000" + set trunc_ckvhash "000000000000" + } + + # Generate JA4H fingerprint string + set ja4h_fp "${me}${v}${c}${r}${hc}${lang}_${trunc_hhash}_${trunc_chash}_${trunc_ckvhash}" + HTTP::header insert "X-JA4H" $ja4h_fp + + # If enabled, Generate JA4H_r(o) fingerprint string + if { ${ja4h_raw} } { + set ja4hr_fp "${me}${v}${c}${r}${hc}${lang}_${hstr}_${cstr}_${ckvstr}" + set ja4h_xhdr "X-JA4H_r" + if { ${ja4h_original} } { + set ja4h_xhdr "X-JA4H_ro" + } + } +} +when ACCESS_ACL_ALLOWED { + ACCESS::session data set session.custom.JA4h $ja4h_fp + + +} diff --git a/Access/ja4l-access.irule b/Access/ja4l-access.irule new file mode 100644 index 0000000..2515d29 --- /dev/null +++ b/Access/ja4l-access.irule @@ -0,0 +1,56 @@ +############################################################################################### +# iRule to calculate JA4L "light distance" +# See JA4L spec on GitHub for more details +# https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4L.md +# and information on calculating different distance/latency items. +# +# Copyright (c) 2024, FoxIO, LLC. +# All rights reserved. +# Licensed under FoxIO License 1.1 +# For full license text and more details, see the repo root https://github.com/FoxIO-LLC/ja4 +############################################################################################### + +when FLOW_INIT { + #Set timestamp for initial received SYN + set ja4l_ts1 [clock clicks] + #log local0. "ja4l_ts1: ${ja4l_ts1}" +} + +when CLIENT_ACCEPTED { + #Set timestamp for final received SYN/ACK + set ja4l_ts2 [clock clicks] + #log local0. "ja4l_ts2: ${ja4l_ts2}" + + #Calculate time difference - (ts2-ts1)/2 + set ja4l_tcp_latency [expr ($ja4l_ts2 - $ja4l_ts1)/2] + + # Get IP TTL + set ttl [IP::ttl] + +} + +when CLIENTSSL_CLIENTHELLO { + set ja4l_ts3 [clock clicks] +} + +when CLIENTSSL_HANDSHAKE { + set ja4l_ts4 [clock clicks] + # Calculate "application latency" + set ja4l_app_latency [expr ($ja4l_ts4 - $ja4l_ts3)/2] + + # Defone JA4L string + set ja4l "${ja4l_tcp_latency}_${ttl}_${ja4l_app_latency}" + +} + + + +when ACCESS_POLICY_AGENT_EVENT { + if { [ACCESS::policy agent_id] eq "JA4FPL" } { + ACCESS::session data set session.custom.JA4l $ja4l + ACCESS::session data set session.custom.JA4la [getfield $ja4l "_" 1] + ACCESS::session data set session.custom.JA4lb [getfield $ja4l "_" 2] + ACCESS::session data set session.custom.JA4lc [getfield $ja4l "_" 3] + + } +}