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