HTTP Brute Force Mitigation Playbook: Appendix
This is the HTTP Brute Force Mitigation Playbook: Appendix where some the sample configurations are located.
Mitigation: TLS Fingerprint
To use the TLS Fingerprint iRules, create separate iRules in the Configuration Utility for iRule 1 - FingerprintTLS proc and iRule 2 - the rate limiting iRule.
Apply TLS Fingerprint Rate Limiting iRule to a Virtual Server that needs to be protected.
See "Mitigation: TLS Fingerprint" section in Chapter 3 - BIG-IP LTM Mitigation Options for HTTP Brute Force Attacks for sample internal and external Data Groups configuration.
External Data Group fingerprint_db
Internal Data Group malicious_fingerprintdb
In edit mode, String and Value are exposed
## iRule #1 - FingerprintTLS-proc
## iRule #1 - FingerprintTLS-proc
# from Library-Rule in
# https://devcentral.f5.com/s/articles/tls-fingerprinting-a-method-for-identifying-a-tls-client-#without-decrypting-24598
## TLS Fingerprint Procedure #################
##
## Author: Kevin Stewart, 12/2016
## Derived from Lee Brotherston's "tls-fingerprinting" project @ https://github.com/LeeBrotherston/tls-fingerprinting
## Purpose: to identify the user agent based on unique characteristics of the TLS ClientHello message
## Input:
## Full TCP payload collected in CLIENT_DATA event of a TLS handshake ClientHello message
## Record length (rlen)
## TLS outer version (outer)
## TLS inner version (inner)
## Client IP
## Server IP
##############################################
proc fingerprintTLS { payload rlen outer inner clientip serverip } {
## The first 43 bytes of a ClientHello message are the record type, TLS versions, some length values and the
## handshake type. We should already know this stuff from the calling iRule. We're also going to be walking the
## packet, so the field_offset variable will be used to track where we are.
set field_offset 43
## The first value in the payload after the offset is the session ID, which may be empty. Grab the session ID length
## value and move the field_offset variable that many bytes forward to skip it.
binary scan ${payload} @${field_offset}c sessID_len
set field_offset [expr {${field_offset} + 1 + ${sessID_len}}]
## The next value in the payload is the ciphersuite list length (how big the ciphersuite list is. We need the binary
## and hex values of this data.
binary scan ${payload} @${field_offset}S cipherList_len
binary scan ${payload} @${field_offset}H4 cipherList_len_hex
set cipherList_len_hex_text ${cipherList_len_hex}
## Now that we have the ciphersuite list length, let's offset the field_offset variable to skip over the length (2) bytes
## and go get the ciphersuite list. Multiple by 2 to get the number of appropriate hex characters.
set field_offset [expr {${field_offset} + 2}]
set cipherList_len_hex [expr {${cipherList_len} * 2}]
binary scan ${payload} @${field_offset}H${cipherList_len_hex} cipherlist
## Next is the compression method length and compression method. First move field_offset to skip past the ciphersuite
## list, then grab the compression method length. Then move field_offset past the length (2) bytes and grab the
## compression method value. Finally, move field_offset past the compression method bytes.
set field_offset [expr {${field_offset} + ${cipherList_len}}]
binary scan ${payload} @${field_offset}c compression_len
#set field_offset [expr {${field_offset} + ${compression_len}}]
set field_offset [expr {${field_offset} + 1}]
binary scan ${payload} @${field_offset}H[expr {${compression_len} * 2}] compression_type
set field_offset [expr {${field_offset} + ${compression_len}}]
## We should be in the extensions section now, so we're going to just run through the remaining data and
## pick out the extensions as we go. But first let's make sure there's more record data left, based on
## the current field_offset vs. rlen.
if { [expr {${field_offset} < ${rlen}}] } {
## There's extension data, so let's go get it. Skip the first 2 bytes that are the extensions length
set field_offset [expr {${field_offset} + 2}]
## Make a variable to store the extension types we find
set extensions_list ""
## Pad rlen by 1 byte
set rlen [expr {${rlen} + 1}]
while { [expr {${field_offset} <= ${rlen}}] } {
## Grab the first 2 bytes to determine the extension type
binary scan ${payload} @${field_offset}H4 ext
## Store the extension in the extensions_list variable
append extensions_list ${ext}
## Increment field_offset past the 2 bytes of the extension type
set field_offset [expr {${field_offset} + 2}]
## Grab the 2 bytes of extension lenth
binary scan ${payload} @${field_offset}S ext_len
## Increment field_offset past the 2 bytes of the extension length
set field_offset [expr {${field_offset} + 2}]
## Look for specific extension types in case these need to increment the field_offset (and because we need their values)
switch $ext {
"000b" {
## ec_point_format - there's another 1 byte after length
## Grab the extension data
binary scan ${payload} @[expr {${field_offset} + 1}]H[expr {(${ext_len} - 1) * 2}] ext_data
set ec_point_format ${ext_data}
}
"000a" {
## elliptic_curves - there's another 2 bytes after length
## Grab the extension data
binary scan ${payload} @[expr {${field_offset} + 2}]H[expr {(${ext_len} - 2) * 2}] ext_data
set elliptic_curves ${ext_data}
}
"000d" {
## sig_alg - there's another 2 bytes after length
## Grab the extension data
binary scan ${payload} @[expr {${field_offset} + 2}]H[expr {(${ext_len} - 2) * 2}] ext_data
set sig_alg ${ext_data}
}
default {
## Grab the otherwise unknown extension data
binary scan ${payload} @${field_offset}H[expr {${ext_len} * 2}] ext_data
}
}
## Increment the field_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}}]
}
}
## Now let's compile all of that data.
set cipl [string toupper ${cipherList_len_hex_text}]
set ciph [string toupper ${cipherlist}]
set coml ${compression_len}
set comp [string toupper ${compression_type}]
if { ( [info exists extensions_list] ) and ( ${extensions_list} ne "" ) } { set exte [string toupper ${extensions_list}] } else { set exte "@@@@" }
if { ( [info exists elliptic_curves] ) and ( ${elliptic_curves} ne "" ) } { set ecur [string toupper ${elliptic_curves}] } else { set ecur "@@@@" }
if { ( [info exists sig_alg] ) and ( ${sig_alg} ne "" ) } { set siga [string toupper ${sig_alg}] } else { set siga "@@@@" }
if { ( [info exists ec_point_format] ) and ( ${ec_point_format} ne "" ) } { set ecfp [string toupper ${ec_point_format}] } else { set ecfp "@@@@" }
## Initialize the match variable
set match ""
## Now let's build the fingerprint string and search the database
set fingerprint_str "${outer}+${inner}+${cipl}+${ciph}+${coml}+${comp}+${exte}+${ecur}+${siga}+${ecfp}"
## Un-comment this line to display the fingerprint string in the LTM log for troubleshooting
#log local0. "${clientip}-${serverip}: fingerprint_str = ${fingerprint_str}"
if { [class match ${fingerprint_str} equals fingerprint_db] } {
## Direct match
set match [class match -value ${fingerprint_str} equals fingerprint_db]
} elseif { not ( ${ciph} starts_with "C0" ) and not ( ${ciph} starts_with "00" ) } {
## Hmm.. there's no direct match, which could either mean a database entry doesn't exist, or Chrome (and Opera) are adding
## special values to the cipherlist, extensions list and elliptic curves list.
## ex. 9A9A, 5A5A, EAEA, BABA, etc. at the beginning of the cipherlist
## Let's strip out these anomalous values and try the match again.
## Substract 2 bytes from cipherlist length
set cipl [format %04x [expr {{[expr 0x${cipl}] - 2}}]]
## Subtract 2 bytes from the front of the cipher list
set ciph [string range ${ciph} 4 end]
## Subtract 2 bytes from the front of the extensions list
set exte [string range ${exte} 4 end]
## There might be an additional random set in the string that needs to be removed (pattern is "(.)A\1A")
regsub {(.)A\1A} ${exte} "" exte
## If the above regsub doesn't work, try the following:
#regsub {(\wA)\1} ${exte} "" exte
## Subtract 2 bytes from the front of the elliptic curves list
set ecur [string range ${ecur} 4 end]
## Rebuild the fingerprint string
set fingerprint_str "${outer}+${inner}+${cipl}+${ciph}+${coml}+${comp}+${exte}+${ecur}+${siga}+${ecfp}"
if { [class match ${fingerprint_str} equals fingerprint_db] } {
## Guess match
set match [class match -value ${fingerprint_str} equals fingerprint_db]
log local0. "guessing fingerprint ${fingerprint_str}"
} else {
## No match
set match "${fingerprint_str}"
#set match ""
log local0. "no matched fingerprint ${match}"
}
}
else {
set match "${fingerprint_str}"
#log local0. "no matched fingerprint ${fingerprint_str}"
}
## Return the matching user agent string
return ${match}
}
##irule #2 - FingerprintTLS-irule apply to VS
##irule #2 - FingerprintTLS-irule apply to VS
## define variables for rate limiting
when RULE_INIT {
# Default rate to limit requests
set static::maxRate 15
# Default rate to
set static::warnRate 12
# During this many seconds
set static::timeout 1
}
when CLIENT_ACCEPTED {
## 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 outer_sslver rlen hs_type rilen inner_sslver
if { ( ${rtype} == 22 ) and ( ${hs_type} == 1 ) } {
## This is a TLS ClientHello message (22 = TLS handshake, 1 = ClientHello)
## Call the fingerprintTLS proc from the FingerprintTLS-proc iRule to set the TLS fingerprint value with record length, outer and inner SSL version info, source and destination IP address details as input
set fingerprint [call FingerprintTLS-proc::fingerprintTLS [TCP::payload] ${rlen} ${outer_sslver} ${inner_sslver} [IP::client_addr] [IP::local_addr]]
### Do Something here ###
# set matched ${fingerprint}
#log local0. "fingerprint is ${fingerprint}"
# if { [class match $matched equals malicious_fingerprint] } {
# log local0. "fingerprint is $matched dropped"
# drop
# }
###########################
#rate limit logic
#check if fingerprint matches an expected fingerprint
if { [class match ${fingerprint} equals fingerprint_db] } {
event disable all
} elseif {![class match ${fingerprint} equals fingerprint_db] && [class match ${fingerprint} equals malicious_fingerprintdb]} {
#check if fingerprint matches a known malicious fingerprint
set malicious_fingerprint [class match -value ${fingerprint} equals malicious_fingerprintdb]
drop
log local0. "known malicious fingerprint matched $malicious_fingerprint"
} else {
set suspicious_fingerprint ${fingerprint}
#rate limit fingerprint
# Increment and Get the current request count bucket
#set epoch [clock seconds]
#monitor an unrecognized fingerprint and rate limit it
set currentCount [table incr -mustexist "Count_[IP::client_addr]_${suspicious_fingerprint}"]
if { $currentCount eq "" } {
# Initialize a new request count bucket
table set "Count_[IP::client_addr]_${suspicious_fingerprint}" 1 indef $static::timeout
set currentCount 1
}
# Actually check fingerprint for being over limit
if { $currentCount >= $static::maxRate } {
log local0. "ERROR: fingerprint:[IP::client_addr]_${suspicious_fingerprint} exceeded ${static::maxRate} requests per second. Rejecting request. Current requests: ${currentCount}."
event disable all
drop
}
if { $currentCount > $static::warnRate } {
#log local0. "WARNING: fingerprint:[IP::client_addr]_${suspicious_fingerprint} exceeded ${static::warnRate} requests per second. Will reject at ${static::maxRate}. Current requests: ${currentCount}."
}
log local0. "fingerprint:[IP::client_addr]_${suspicious_fingerprint}: currentCount: ${currentCount}"
}
}
}
### Do Something here ###
# Collect the rest of the record if necessary
if { [TCP::payload length] < $rlen } {
TCP::collect $rlen
}
## Release the paylaod
TCP::release
}
#client_data close bracket
when HTTP_REQUEST {
if { $currentCount > $static::warnRate } {
log local0. "WARNING: suspicious_fingerprint: [IP::client_addr]_${suspicious_fingerprint}: User-Agent:[HTTP::header User-Agent] exceeded ${static::warnRate} requests per second. Will reject at ${static::maxRate}. Current requests: ${currentCount}."
}
}
Published Apr 28, 2020
Version 1.0ArvinF
SIRT
I'm Arvin, Security Engineer with the F5SIRTArvinF
SIRT
I'm Arvin, Security Engineer with the F5SIRTNo CommentsBe the first to comment