Prevent User Enumeration in WordPress

We recently had to deal with a hacking attempt against a client WordPress site that had a few interesting aspects. The fix involved additional .htaccess rules to block user enumeration.

We experienced multiple failed login attempts against WordPress. This wasn’t particularly worrying – the originating IP address was automatically blocked by our Fail2Ban setup after three unsuccessful attempts. The attacker (probably a script) then switched IP address and repeated the process, trigerring a further ban and repeating the cycle. The attack lasted for approximately 10 minutes and triggered more than 50 bans.

The attack focused on usernames that were very close to (but not actually the same as) actual usernames on the site. It looked like a partially-successful user-enumeration attempt made up the initial phase of the attack. Puzzlingly, only some usernames had been enumerated.

User Enumeration

User Enumeration is when would-be attackers collect usernames by interacting with your app. Unfortunately, by default WordPress makes this process easy. Entering http://example.com/?author=1 in the browser will trigger display of all articles authored by the user with an ID of ‘1’ – along with their registered username. This provides would-be attackers with a toe-hold – they can attempt to log in to valid usernames rather than having to guess.

Our usual setup involves user-enumeration prevention measures – so it was surprising to see (almost valid) usernames cropping up in the log.

It turns out that our user-enumeration prevention relied on ‘redirect_canonical’ WordPress filter. This filter is triggered if you navigate to http://example.com/?author=1 – in this case, it performs a redirect to the Author archives for the author with an ID of 1.

The problem: If a registered user on the site has not authored any articles, the redirect will not take place. The user does not have an archive, the redirect doesn’t take place, and the user-enumeration can proceed.

In our case, the enumerated users had a custom membership role rather than an author role – so they will never have an archive page. In our context, these are pretty low risk users, with very few permissions on the site. Nevertheless, it’s a pain having to check when these attacks occur, and it places unecessary load on the server.

We verified the partially successful enumeration attempt by doing some penetration testing using WPScan – this turned up the exact “usernames” that were tried during the hack attempt.

The solution involved extra .htaccess rules to prevent user-enumeration. We also added some extra rules to block login attempts using the enumerated (incorrect) usernames – just in case the attacker is logging them for future usage.

.htaccess Rule to Prevent User enumeration


RewriteEngine On
%{REQUEST_URI} !^/wp-admin [NC]
RewriteCond %{QUERY_STRING} author=\d
RewriteRule (.*) $1? [L,R=301]

Explanation

Line One

Turn on rewriting functionality – the Apache mod_rewrite module must be installed on the server. This module rewrites requested URLs on the fly by means of a rule-based rewriting engine. The rewrite engine is based on a Perl Compatible Regular Expressions(PCRE) parser.

Line 2

Apply a rewrite condition such that the rule will be ignored if the REQUEST_URI begins with /wp-admin.

REQUEST_URI

The path component of the requested URI, such as “/index.html”. This notably excludes the query string which is available as its own variable named QUERY_STRING. — Apache mod_rewrite Docs

REQUEST_URI in simple terms is the bit after your domain.

The author=\d string that we’ll use to match the user enumeration attempt is used legitimately to display author posts in back end – so the rewrite rule should not apply if the request takes place in the WordPress admin area.

Line 3

Specify the rewrite condition – the target query string must include 'author=\d', where \d means a single digit.

This means that http://example.com/?dummy&author=1 will trigger the rewrite, as will http://example.com/?dummy&author=100 – provided we’re not in the admin area, as specified by the previous condition.

Note that the rule doesn’t specify that the ‘author’ variable is at the start of the query string (e.g. ^/?author=([0-9]*) – a query string that starts with /?author= followed by any number of digits).

Line 4

The rewrite rule: replace the entire path (.*) with itself $1 but with an empty query string ?.

Make this the last rule and specify that it is a permanent redirect [L,R=301].

TLDR: .htaccess Rules

Add these rules to .htaccess to prevent all malicious user-enumeration attempts. Such attempts will redirect to the site home page:



RewriteEngine On
RewriteCond %{REQUEST_URI} !^/wp-admin [NC]
RewriteCond %{QUERY_STRING} author=\d
RewriteRule (.*) $1? [L,R=301]

Note that preventing user-enumeration is only one component of an effective security policy.

References

  • DavidCWebs

    Test comment