Preventing Brute Force Password Guessing Attacks with APM - Part 3
F5er and DevCentral community member ystephie is back with another great solution (check out her first solution here: BIG-IP APM Customized Logon Page), this time tackling brute force attacks utilizing customizations with the BIG-IP Access Policy Manager. This solution requires BIG-IP 10.2.2 Hotfix 1 or later.
Introduction
Exposing applications or services to the Internet opens inherent security risks. BIG-IP Access Policy Manager (APM) provides edge authentication and access control services for applications, BIG-IP Edge Gateway provides secure SSL VPN services, and BIG-IP Application Security Manager (ASM) provides protection against a variety of attacks. In this series of APM deployment examples, we cover a couple techniques for protecting against brute force password-guessing attacks.
In our first example (Part 1), we walked through the process of including a CAPTCHA on the APM logon page via a web service (Google reCAPTCHA project), to provide some protection against script based or other automated attacks.
In our second example (Part 2), we modified our configuration to only display the CAPTCHA challenge if a user has previously failed authentication (by checking the user’s badPwdCount attribute from Active Directory).
In this third example, we’ll build on the previous example but keep track of authentication failures ourselves on box (through use of an iRules Session Table). This will remove the dependency on Active Directory and solves the issue around tracking failures for invalid users (providing the same external behavior, whether a user is valid or invalid).
Replacing the AD Query with an iRules Session Table
As we explained in the previous example, we’d rather not inconvenience users by forcing a CAPTCHA challenge on them unless we start seeing authentication failures. Using iRules, we can create a Session Table that keeps track of all the usernames and number of failed authentication attempts for each user. Since we’re tracking all authentication failures (for both valid and invalid users), we can increase the number of failures before showing the CAPTCHA challenge without giving a hacker any hint about who might be a valid user (opening for a username guessing attack). With the Active Directory badPwdCount technique, we could only track failures for valid AD users. With the Session Table iRules approach, you can set policies around how long before resetting a user’s bad authentication count, as well as how many authentication failures to allow before forcing a CAPTCHA challenge. Here’s a bit more on the iRules Table command:
Some pretty powerful stuff here, and a good place to track other bits of information across APM user sessions. For example, this example could be extended to track and compare geo IP location data across user sessions to detect suspicious activity. (See APM Access Policy Geolocation for an example where we look up a user’s geolocation information via iRules.) In this example, we’ll be using a combined logon page with both username/password and CAPTCHA challenge as in Part 1. Before we begin, please create a new access policy, set up an HTTP Auth Agent as described in Part 1, as well as an authentication server (e.g. Active Directory, RADIUS, LDAP).
Access Policy Overview
The following behavior is what we want to accomplish with our access policy.
1. A user wants to logon for the first time. He’ll see the standard APM logon page.
- If the user passes our Active Directory authentication (or your choice of auth factor), the user will be allowed access to the SSL VPN.
- If the user fails authentication, we want to first add him to our Session Table and check to see if the user has maxed out his authentication tries (set in the iRule). Please see logic in red below.
when ACCESS_POLICY_AGENT_EVENT {
# Maxtries is a variable that sets how many times you want the regular
# logon page to show before showing the CAPTCHA. Timeout is a variable
# that sets how long a user entry will persist in the session table.
# This is the only place you need to change these variables.
set maxtries 2
set timeout 900
set user [ACCESS::session data get session.logon.last.username]
ACCESS::session data set session.custom.maxtries $maxtries
if {[ACCESS::policy agent_id] eq "session lookups"} {
set badpwd [table lookup $user]
if {$badpwd == {}} {
set badpwd [table add $user 0 $timeout]
}
ACCESS::session data set session.custom.badpwdcount $badpwd
# log local0. "$user has this number of incorrect logons: $badpwd"
}
if {[ACCESS::policy agent_id] eq "badpwd"} {
table incr $user
}
if {[ACCESS::policy agent_id] eq "goodpwd"} {
table delete $user
}
}
- If we detect that the user maxed out his authentication tries, the next logon page should be one with a CAPTCHA challenge included.
- If we detect that the user hasn’t maxed out his authentication tries, the next logon page should be the APM logon page with some helpful text that tells the user that his credentials were incorrect and to try again.
Note: The user will continue to return to this logon page with the error message until they exceed the max tries threshold. Once the user exceeds the threshold, we need to take them to the logon page with the CAPTCHA challenge.
2. A user comes back to the logon page after closing the previous (failed) session. Since this user is already in our Session Table, we can check immediately how many times this user has failed authentication.
- If this user hasn’t reached the max authentication tries threshold, we can send him to the error message logon page as usual.
- If this user has already reached the max tries threshold, we run into a problem. Should we send them directly to the CAPTCHA challenge page and ignore the credentials they just entered? Or can we just ask them to enter a CAPTCHA challenge without reentering their credentials? In our example, we’re doing the latter.
How can we make it so that the various types of logon pages come one after another seamlessly without creating a very long access policy? A neat trick we can use is APM’s redirect ending. The redirect ending can be used to send a user to any URL. But in our case, we want to direct the access policy to return to the very beginning of the access policy (the “Start” block), without asking the user to click on a link such as in a deny ending.
After clicking Edit Endings in the VPE, add two redirect endings the way I have it above. The URL field should be the virtual server with “/captcha” or “/error” appended to it. Those paths allow us to pass some context about the results of the previous run through the access policy. With each new access policy run, all information about the previous run is forgotten (a new session is created). But, what if I needed to know if the user had already maxed out their authentication failures and needed the CAPTCHA challenge? That’s how these redirects come into play. We can just attach a path to the end of the redirect URL.
Now that we know how to redirect users back to the start of access policy, how do we tell “/captcha” apart from “/error”? Well, with a block called Landing URI, we can change the behavior of a new run through the access policy depending on the results from the previous run. In the branch rules of the Landing URI block have the following expressions under advanced. The Landing URI block looks for the landing uri which is the path we appended in the redirect ending.
Name: | Expression: |
Direct to Captcha | expr { [mcget {session.server.landinguri}] == “/captcha” || [mcget {session.server.landinguri}] == “/captcha/” } |
Direct to logon page with err msg | expr { [mcget {session.server.landinguri}] == “/error” || [mcget {session.server.landinguri}] == “/error/” } |
With this, we can take a different path depending on the information passed from the redirect ending (captcha or error). The following is the access policy where you can see both the initial Landing URI block and final Redirect endings.
Let’s now step through the paths a user may follow, and review the logic in the access policy.
1. The user follows the “fallback” normal path (no previous redirect), enters their credentials, and goes through the iRule lookups and Auth macro. This macro will help us determine if they’re allowed access to the SSL VPN service or web app, sent to the Error Logon Page (via the Redirect to Error ending), or Captcha Logon Page (via the Redirect to Captcha ending).
2. The user fails authentication somewhere in the access policy but not enough times to reach maxtries. In this case, the user gets sent to the Error Logon Page via the Redirect to Error redirect ending. The Error Logon Page block is the regular logon page with a message that informs the user that they have entered incorrect credentials.
- To add the error message to the standard logon page, add the following html code to Form Header Text in the VPE:
3. The user either failed authentication somewhere in the access policy and reached maxtries, or failed the CAPTCHA challenge inside the Captcha Auth w AD Auth macro above. In either of these cases, the user gets redirected to CAPTCHA Auth w AD Auth (again with the Redirect to Captcha redirect ending) which has a CAPTCHA challenge page with username/password fields. We will continue to redirect to this page until the user enters correct credentials and CAPTCHA challenge.
Visual Policy Editor Macros
Macros are an important addition in the VPE because it allows us to manage one configuration block and use it in several places within an access policy. This way, we avoid having to change or add blocks that have the same behavior in more than one place within the VPE. Macros are also a great way to maintain a simple to understand top level policy (readability).
There are four macros total in the access policy. You can see Captcha Auth w AD Auth and iRule Lookups and Auth in this top-level access policy. There are two additional macros (AD Auth and iRule and Verify with Captcha) that are used within the first two macros (nested macros). The purpose of the macro AD Auth and iRule is used for authentication and to trigger an iRule for updating our Session Table (for tracking users and authentication failures). On authentication failure, it also compares the bad authentication count with maxtries to determine where to send the user next.
This macro first authenticates the user (please remember to change the Max Logon Attempts Allowed in the AD Auth to 1).
If the user passes authentication, the iRule Event goodpwd (with ID “goodpwd”) triggers an iRule event that removes the user from the Session Table.
when ACCESS_POLICY_AGENT_EVENT {...if {[ACCESS::policy agent_id] eq "goodpwd"} {# delete the user, they passed authentication,
# so we don't need to remember their badpwdcount anymore
table delete $user
}}
The user is then assigned resources and allowed access (to the SSL VPN service or web application). If the user fails authentication, the iRule Event badpwd (with ID “badpwd”) triggers an iRule event that increments the user’s bad password count by one.
when ACCESS_POLICY_AGENT_EVENT {…if {[ACCESS::policy agent_id] eq "badpwd"} {# Increment the badpwdcount,
# This user just failed authentication.
table incr $user}…}
The Variable Assign block with the following configuration, increments the bad password count within the active executing policy.
Custom Variable: | Custom Expression: |
session.custom.badpwdcount | expr { [mcget {session.custom.badpwdcount}]+1} |
After the user fails authentication once, we must be sure to compare the badpwdcount (custom session variable name for bad password count) against maxtries (custom session variable name for max tries) so that we can determine whether or not to send them to a logon page with CAPTCHA challenge next or to a regular logon page.
Please note that session.custom.maxtries is a custom APM session variable initialized as part of the iRule. The session.custom.badpwdcount APM session variable is also set as part of the iRule shown later (count pulled from the iRule table).
We accomplish this comparison using an Empty block with the following branch rule.
Name: | Expression: |
User may try again | expr { [mcget {session.custom.badpwdcount}] < [mcget {session.custom.maxtries}] } |
You may have noticed that the endings shown (Successful, Redirect to Error, and Redirect to Captcha) are different from the default endings. To edit endings, click on Edit Terminals and add/edit the terminals to match the figure below. You can also edit the colors.
The macro, Captcha Auth w AD Auth is there to display the logon page with CAPTCHA challenge and authenticate the CAPTCHA via the HTTP Auth agent. If the user passes the CAPTCHA, we will authenticate them with our regular Active Directory auth agent.
In the access policy, the Landing URI will direct to this macro (Captcha Auth w AD Auth) if it sees that the landing uri contains the string “/captcha”.
Note that the Captcha Auth w AD Auth macro above is almost the same as the policy you created in Part 1 of this series, but includes the AD Auth and iRule macro covered earlier.
The next macro is a slight variation of the macro above. In fact the only difference is that Captcha Logon Page block above has username/password inputs plus the CAPTCHA challenge while Verify Logon Page block below only has the CAPTCHA challenge like we created in Part 2 of this series.
The reason we have the macro below is to avoid asking a user to re-enter their credentials again (if we determine they need to pass the CAPTCHA challenge only after already collecting their username and password).
The last and final macro below helps us decide if we should authenticate the user or send the user to a CAPTCHA challenge.
We start with the iRule Event session lookups block (with ID “session lookups”).
when ACCESS_POLICY_AGENT_EVENT {# Maxtries is a variable that sets how many times you want the regular
# logon page to show before showing the CAPTCHA. Timeout is a variable
# that sets how long a user entry will persist in the session table.
# This is the only place you need to change these variables.
# we set how many tries we want to give a user here
set maxtries 2
# we set how long we want to keep a user in the Session Table here
set timeout 900
# we retrieve the username information from the session var
set user [ACCESS::session data get session.logon.last.username]# set the session variable maxtries so we can compare it to our badpwdcount
ACCESS::session data set session.custom.maxtries $maxtriesif {[ACCESS::policy agent_id] eq "session lookups"} {# look up the user, if the user isn’t already in the Session Table, add it.
set badpwd [table lookup $user]if {$badpwd == {}} {
set badpwd [table add $user 0 $timeout]}# set the session variable badpwdcount so we can compare it to our maxtries
ACCESS::session data set session.custom.badpwdcount $badpwd# log local0. "$user has this number of incorrect logons: $badpwd"
}…}
We will trigger an iRule that checks our iRule session table and determines how many badpwdcounts a particular user has. If a user has not been entered into our database, this event adds the user to the table with badpwdcount equal to zero. The iRule block also has several branch rules shown below.
Name: | Expression: |
Send users to the CAPTCHA | expr { [mcget {session.custom.badpwdcount}] >= [mcget {session.custom.maxtries}] } |
The user may authenticate | expr { [mcget {session.custom.badpwdcount}] < [mcget {session.custom.maxtries}] } |
Let’s review the behavior here:
1. The user has failed authentication too many times, we will go down the Send users to the Captcha branch. When we reach the Verify with Captcha Page macro, the user is prompted with a plain CAPTCHA challenge. By only showing the plain CAPTCHA challenge, the user doesn’t have to reenter their credentials. If they pass the CAPTCHA, they are allowed to authenticate against AD. If they fail either the CAPTCHA challenge or AD Auth, they’ll be redirected to the beginning of the access policy.
2. If the user hasn’t failed authentication too many times, we will authenticate them right away using the AD Auth and iRule macro.
Access Policy – Putting it all Together
Now we’re ready to revisit our complete access policy. Please see below.
We were able to accomplish the desired behavior with our access policy with the help of iRule Session Tables, redirect endings, macros and Landing URI functionality. With the combined help of the Landing URI and redirect ending, we make switching between different logon pages seamless. Using Session Tables we were able to track authentication failures for all users, valid or not. The point of tracking all users (whether they exist in Active Directory or not), is to ensure that a hacker can’t see any difference in behavior and guess valid user names. Finally, macros help us simplify our access policy (more readable). The last step is to attach the following iRule to the access policy.
when ACCESS_POLICY_AGENT_EVENT {# Maxtries is a variable that sets how many times you want the regular
# logon page to show before showing the CAPTCHA. Timeout is a variable
# that sets how long a user entry will persist in the session table.
# This is the only place you need to change these variables.
set maxtries 2
set timeout 900
set user [ACCESS::session data get session.logon.last.username]ACCESS::session data set session.custom.maxtries $maxtriesif {[ACCESS::policy agent_id] eq "session lookups"} {set badpwd [table lookup $user]if {$badpwd == {}} {
set badpwd [table add $user 0 $timeout]}ACCESS::session data set session.custom.badpwdcount $badpwd# log local0. "$user has this number of incorrect logons: $badpwd"
}if {[ACCESS::policy agent_id] eq "badpwd"} {table incr $user}if {[ACCESS::policy agent_id] eq "goodpwd"} {table delete $user
}}
Final Notes
This solution is a more complex example where we created an advanced access policy with macros, along with APM iRules events, redirect endings, and iRules plus session table, to track authentication failures across user sessions. In part 4, we’ll make some minor modifications to this policy to also support a temporary lockout of users after failing authentication too many times. This provides a more complete solution for blocking credentials guessing attacks, and can help protect against DOS attacks towards internal AD servers (where it might be possible to lock a user out of the internal corporate Active Directory server via too many authentication failures).
About the Author
Stephanie is a summer intern at F5, heading back to school soon to continue her EECS degree at UC Berkeley, and has been having a blast creating interesting solutions for BIG-IP. Stephanie’s passion for engineering, and smile, is contagious.
- Songseajoon_222Nimbostratus
Where does goodpwd set?