WordPress brute-force attack protection in a production environment.

Following the benchmark tests that we published last year, this article will focus on NinjaFirewall in a production environment, facing two real brute-force attacks.

The victim

It is a small VPS with the following specifications:

  • Intel Xeon L5640 @ 2.27GHz
  • 1 GB of RAM
  • Nginx/1.6.1 + PHP-FPM/5.3.29
  • 64-bit Linux kernel 3.2.58-xenU-11-50785a6-x86_64
  • munin-node for monitoring
  • WordPress 4.0 with NinjaFirewall (WP+ edition) 1.0.6

There is no specific performance optimization, the VPS server is hosting one single WordPress blog having only +/-250 unique visitors a day.

The first attack

The attack came from IP 217.199.161.42, a Plesk server that was apparently compromised. It started on September 11, 2014 at 06:44:36PM (+0200) and stopped 16 minutes and 15 seconds later, at 07:00:51PM. During those 975 seconds, it sent 30,607 HTTP POST requests to the wp-login.php page.
With an average of 31 requests per second, this is quite a large and unusual attack. Most of them will not send more than 10 requests per second because, unlike a DOS attack, their main goal is to find users password, not to take the blog down (if the site became unreachable, the brute-force attack would fail).

A sample of the HTTP server log shows that it even reached up to 36 POST requests per seconds:

217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"
217.199.161.42 - - [11/Sep/2014:19:00:39 +0200] "POST /wp-login.php HTTP/1.0" 401 606 "-" "-" "-" "[obscured]"

The full HTTP access log can be downloaded here: server_access.log.tgz (15Kb)

On the first munin-node graph, we can see the 16-minute incoming attack, reaching 107 kbit/s on the eth2 interface:

The second graph shows the number of connections and their respective state at the kernel firewall level. During the attack, there were 3k+ connections in the TIME_WAIT state:

The HTTP server, Nginx, had to deal with an average of 31 requests per seconds:

Munin’s interrupts & context switches graph during the attack:

Now, we can have a look at how NinjaFirewall dealt with the attack: the CPU usage graph shows that it barely reached 5% (out of 200%):


The second attack

This attack came from Romanian IP 89.45.249.30. It started on October 11, 2014 at 08:40:44PM (+0200) and stopped 1 hour, 2 minutes and 43 seconds later, at 09:43:27PM. During those 3,763 seconds, it sent 27,139 HTTP POST requests to the wp-login.php page.
With an average of 7 requests per second, this is typical attack.

A sample of the HTTP server log shows that it reached up to 10 POST requests per seconds:

89.45.249.30 - - [11/Oct/2014:21:43:24 +0200] "POST /wp-login.php HTTP/1.0" 401 610 "-" "-" "-" "obscured"
89.45.249.30 - - [11/Oct/2014:21:43:24 +0200] "POST /wp-login.php HTTP/1.0" 401 610 "-" "-" "-" "obscured"
89.45.249.30 - - [11/Oct/2014:21:43:24 +0200] "POST /wp-login.php HTTP/1.0" 401 610 "-" "-" "-" "obscured"
89.45.249.30 - - [11/Oct/2014:21:43:24 +0200] "POST /wp-login.php HTTP/1.0" 401 610 "-" "-" "-" "obscured"
89.45.249.30 - - [11/Oct/2014:21:43:24 +0200] "POST /wp-login.php HTTP/1.0" 401 610 "-" "-" "-" "obscured"
89.45.249.30 - - [11/Oct/2014:21:43:24 +0200] "POST /wp-login.php HTTP/1.0" 401 610 "-" "-" "-" "obscured"
89.45.249.30 - - [11/Oct/2014:21:43:24 +0200] "POST /wp-login.php HTTP/1.0" 401 610 "-" "-" "-" "obscured"
89.45.249.30 - - [11/Oct/2014:21:43:24 +0200] "POST /wp-login.php HTTP/1.0" 401 610 "-" "-" "-" "obscured"
89.45.249.30 - - [11/Oct/2014:21:43:24 +0200] "POST /wp-login.php HTTP/1.0" 401 610 "-" "-" "-" "obscured"
89.45.249.30 - - [11/Oct/2014:21:43:24 +0200] "POST /wp-login.php HTTP/1.0" 401 610 "-" "-" "-" "obscured"

The full HTTP access log can be downloaded here: server_access.log2.tgz (22Kb)

On the first munin-node graph, we can see the 62-minute incoming attack, reaching 37 kbit/s on the eth2 interface:

The second graph shows that there were +/-400 connections in the TIME_WAIT state:

Nginx had to deal with an average of 7 requests per seconds (max. 10 RPS):

Munin’s interrupts & context switches graph during the attack:

NinjaFirewall performed pretty well, the CPU usage is almost unnoticeable: