For more information regarding the security incident at F5, the actions we are taking to address it, and our ongoing efforts to protect our customers, click here.

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.0
No CommentsBe the first to comment