Protecting Login Pages against Brute Force Attack v1

Problem this snippet solves:

This is the first version of an iRule that can help protect a login site from brute force attacks.

When a request is made against the application the iRule looks for an error text in the server response which increment a counter. That counter has a threshold value and when that has been reached the client IP address is inserted into a blacklist table and completely blocked.

This is a simple solution that can be easily expanded to suit your needs.

Note: ">" should be changed to ">" in the iRule.

How to use this snippet:

Update: iRule using data groups

I just made an updated version of the iRule where the failure text is pulled from a data group instead. This way the iRule becomes more dynamic and "correct" maintenance wise.

Data group "failtext"

ltm data-group internal /Common/failtext {
    records {
        "not allowed" { }
        "unable to login with provided credentials" { }
        "you do not have permission to perform this action" { }
        non_field_errors { }
    }
    type string
}

Code :

when RULE_INIT {
    set static::maxtry 3
    set static::bantime 600
set static::failtext "unable to login with provided credentials"
set static::debug 1
}
when CLIENT_ACCEPTED {
    set srcip [IP::remote_addr]
    if { [table lookup -subtable "blacklist" $srcip] != "" } {
        if {$static::debug} {log "Blocking $srcip"}
        drop
        return
    }
}

when HTTP_REQUEST {
    set login_request 0 
    if {([HTTP::method] equals "POST") and ( [string tolower [HTTP::uri]] equals "/api2/auth-token/" )}{
   set login_request 1
   if {$static::debug} {log "Login attempt from $srcip"}
}
}

when HTTP_RESPONSE {
     if {[HTTP::header "Content-Length"] ne "" && [HTTP::header "Content-Length"] <= 1048576}{
        set content_length [HTTP::header "Content-Length"]
} else {
        set content_length 1048576
}
     # Check if $content_length is not set to 0
     if { $content_length > 0} {
        HTTP::collect $content_length
     }
}

when HTTP_RESPONSE_DATA {
    if {$login_request == 1}{
        if {$static::debug} {log "Payload: [HTTP::payload]"} 
        if { ([string tolower [HTTP::payload]] contains $static::failtext) }{
           if {$static::debug} {log "Login attempt condition failed"}
       set key "count:$srcip"
           set count [table incr $key]
           if {$static::debug} {log "Failed attempt $count"}
           if { $count > $static::maxtry } {
              table add -subtable "blacklist" $srcip "blocked" indef $static::bantime
              table delete $key
              if {$static::debug} {log "Insert into blacklist table"}
              drop
              return
            }
        } 
    }
}

# iRule version 1.1 with data group support
when RULE_INIT {
    set static::maxtry 3
    set static::bantime 600
#set static::failtext "unable to login with provided credentials"
set static::debug 1
}
when CLIENT_ACCEPTED {
    set srcip [IP::remote_addr]
    if { [table lookup -subtable "blacklist" $srcip] != "" } {
        if {$static::debug} {log "Blocking $srcip"}
        drop
        return
    }
}

when HTTP_REQUEST {
    set login_request 0 
    if {([HTTP::method] equals "POST") and ( [string tolower [HTTP::uri]] equals "/api2/auth-token/" )}{
   set login_request 1
   if {$static::debug} {log "Login attempt from $srcip"}
}
}

when HTTP_RESPONSE {
     if {[HTTP::header "Content-Length"] ne "" && [HTTP::header "Content-Length"] <= 1048576}{
        set content_length [HTTP::header "Content-Length"]
} else {
        set content_length 1048576
}
     # Check if $content_length is not set to 0
     if { $content_length > 0} {
        HTTP::collect $content_length
     }
}

when HTTP_RESPONSE_DATA {
    if {$login_request == 1}{
        if {$static::debug} {log "Payload: [HTTP::payload]"} 
        if { [class match [string tolower [HTTP::payload]] contains "failtext"]}{
           if {$static::debug} {log "Login attempt condition failed"}
       set key "count:$srcip"
           set count [table incr $key]
           if {$static::debug} {log "Failed attempt $count"}
           if { $count > $static::maxtry } {
              table add -subtable "blacklist" $srcip "blocked" indef $static::bantime
              table delete $key
              log "Insert into blacklist table"
              drop
              return
            }
        } 
    }
}
Updated Jun 06, 2023
Version 2.0
  • Greetings, could you advise how the blocked IP can be removed later from the blacklist? Thank you.
  • I looked to try this method, it was fine. However, too slow, regardless of the login failure / success or not. Login also slow, slow even fail to log in. I saw reduces the size of content-length, but works quickly and well Login successful when there was still experiencing a slow phenomenon. how can i do?
  • There is no time threshold window per IP, rather simple counter which counts tries without expiration. Please add time between tries or total time window for tries, for example 5 tries in 60 secs.

     

  • @Interhost

     

    There's a default time window which is set in the line 42

     

        set count [table incr $key]
    

    As per the documentation if you don't specify the timeout in the 'table' command it will take default 180s https://clouddocs.f5.com/api/irules/table.html

     

    If you want to specify your own time window you can set the timeout to 'indefinite' and set a 'lifetime' instead in such way

     

        table add $key indefinite  
        set count [table incr -notouch $key]
    

    When the lifetime is reached the counter get's deleted so it starts over...