Blocking WordPress scanners with fail2ban

My web logs are filled with requests for /wp-login.php and /xmlrpc.php, even on sites that aren’t running WordPress. Every one of these attempts is from a scanner trying to find, and possibly exploit, WordPress sites.

Why not put those scanners in a fail2ban jail and block them from further communication with your web server?

Fortunately, someone else had already done most of the work here:
Using Fail2ban on wordpress wp-login.php and xmlrpc.php

I made a few changes to the suggested filter. For example, on this site (osric.com) there is no /wp-login.php, but there are other instances of wp-login.php in subdirectories. Additionally, I am seeing primarily spurious GET requests in my access logs. I modified filter.d/wordpress.conf to look for both GET and POST requests for these WordPress-related files at the site root:

[Definition]
failregex = ^<HOST> .* "(GET|POST) /wp-login.php
            ^<HOST> .* "(GET|POST) /xmlrpc.php

I also made a couple modifications to jail.d/wordpress.conf:

[wordpress]
enabled = true
port = http,https
filter = wordpress
action = iptables-multiport[name=wordpress, port="http,https", protocol=tcp]
logpath = /var/log/custom/log/path/osric.com.access.log
maxretry = 1
bantime = 3600
  1. I set maxretry to 1 so that IP addresses will be banned after the first bad request.
  2. I removed findtime = 600 because that’s the default setting in fail2ban’s jail options.
  3. I increased the bantime from the default (10 minutes) to 1 hour (3600 seconds).

The other change I made was to add /wp-login.php and /xmlrpc.php to my robots.txt file:

User-agent: *
Disallow: /wp-login.php
Disallow: /xmlrpc.php

If a malicious user creates a hyperlink to osric.com/wp-login.php, well-behaved robots should avoid it. This way I’m not inadvertently banning Googlebot, for example.

It feels really good to slam the door in the face of these scanners! But unfortunately I have to ask…

Does this do any good?

I analyzed some of the server logs from before I implemented this block, and as it turns out, most of these are drive-by scanners: they are checking for the presence of a potentially vulnerable page, logging it, and moving on. They are basically gathering reconnaissance for future use.

If a host makes a couple GET or POST requests in the span of one second and then leaves, banning the host’s IP won’t be very effective.

Also, I know that fail2ban is useful for brute-force ssh attempts, but how useful is it for scanners requesting WordPress files? According to my fail2ban logs, over the month of July, 2019:

  • sshd: 69,771 IPs banned
  • WordPress: 4,538‬ IPs banned

wp-login.php isn’t quite the target I thought it was. That is, not compared to sshd.

In any case, it still feels good to block a questionable scanner. Running the following command and viewing the IP addresses that are currently in jail is satisfying:

sudo iptables -v -L f2b-wordpress

7 blocked right now! Take that, suspect IP addresses!

What else can we do to block or prevent WordPress scanners?

A few ideas, none of them earth-shattering:

  • As mentioned in a previous post (Using blocklist.de with fail2ban), the scanner IPs could be shared with a service like blocklist.de
  • Instead of blocking the IP, I could deliver a large file via a slow, throttled connection. This would slow down the scan, but not by much. (One of my colleagues informed me that this practice is called tarpitting.)
  • I could deliver a fake WordPress login form so that the scanners would accumulate unreliable data. Again, this might slow them down, but not by much.

If you have other ideas (even if they aren’t very good!) let me know in the comments.

Thanks

I would like to thank @kentcdodds for inspiring me to dust off this blog post (out of my many unpublished drafts) with his tweet:

Go ahead. Visit https://kentcdodds.com/wp-login.php. I think you’ll enjoy it.

4 thoughts on “Blocking WordPress scanners with fail2ban”

  1. A potential risk with putting an IP address in a fail2ban jail for making an HTTP GET request:

    Malicious users could embed references to wp-login.php on web pages they control. For example:

    <img src="http://example.com/wp-login.php" width="1" height="1" alt="">

    The above could be inserted on a web page, in a forum post, etc. Any user visiting that page would now be in a fail2ban jail and temporarily unable to visit example.com.

  2. Having recently featured on a Google mobile news posting my site got a lot more traffic, and a lot more attention from bad guys (or girls) trying to exploit /wp-login or /xmlrpc weaknesses (and notably more than sshd attempts).

    xmlrpc and wp-login are used by Jetpack and allowed users (including when logging out in the case of wp-login [go figure!]) so it’s worth letting these through, either by including a suitable ignoreregex = , or based up IP (which usefully takes CIDR ranges as well).

    I didn’t enjoy @kentdodds wp-login 😉

  3. “(GET|POST) /wp-login.php” is a legitimate request if you are hosting WordPress, I have seen it when a user is re-authenticating (tip, use fail2ban-regex, to check your filters, to see if you might catch legitimate users).

    However, if you combine with “(GET|POST) /wp-login.php .*200” you will catch scanners/bad actors, since legitimate users should get a redirect.

    There are also lots of non-existant files scanned for, that get 404s, which are obviously not legitimate.

  4. The article mentions using this to detect and block IPs making requests for WordPress files on sites that are not running WordPress.

    A GET request for wp-login.php with a 200 status code is completely normal on a WordPress site. Some themes even include a link to wp-login.php in the footer.

    A legitimate user should get a redirect after a successful POST to wp-login.php.

    A bad password leads to a 200 status code on a POST request, so allowing for a certain number of typos while blocking excessive failures (likely credential stuffing or brute-force attempts) is advisable.

Leave a Reply

Your email address will not be published. Required fields are marked *