One Year’s Worth of WordPress Login Attempts

As you probably know WordPress sites are constantly under attack from all over the world regardless of where your site is hosted or what kind of site it is. Ever since I first noticed suspicious traffic on my own WordPress site many years ago I’ve found it fascinating to watch.

To satisfy some of that curiosity I decided to write a script that collects data on failed logins. I activated it early last year and it’s since then been collecting username, password and a few other things for all login attempts that were not made by a user on my site.

This isn’t a very large dataset of course but I think the results are still worth sharing because they give an indication of some patterns in brute force attacks on any single WordPress site. Please note that I’ve only recorded data from logins made to wp-login.php. xmlrpc.php is blocked on my site. If it’s not blocked on yours it’s good to know that just as many login attempts happen via that endpoint.

Frequency of login attempts

The total number of recorded login attempts on my site during a one year period was 8801. That’s around 24 per day. They do not happen everyday though. Some days it’s completely quiet. Other days there is a barrage. The most active day was May 4th 2018 with 832 logins.

So what does that mean for a WordPress website owner? It simply means that you should expect an ebb and flow and that having 500 or a 1000 login attempts per day is business as usual. Similarly having 0 login attempts one day is normal as well.

The number of unique IPs was 820. That gives an average of 10 attempts per IP but the average is misleading here as well. If we look at the distribution of attempts per IP we see that most IPs only make a few attempts. 672 IPs (82%) of IPs made 3 or less attempts.

See that flat distribution around 100 logins? It means we have a lot of IPs that are making ~100 attempts. I think it’s safe to assume that’s either all the same person or the same exploit kit, perhaps by default configured to make 100 attempts against each site. Consider also that IPs with few attempts are likely part of a distributed attack where requests may come from the same threat actor but with different IPs each time. This is known as a botnet.

Most common usernames and passwords

What do you think was the most common username in the login attempts? I have a feeling most people would guess “admin”. Close! That’s the second most common. The most common one is the domain name stripped of it’s top domain.

The chart above shows all usernames in the data. I was expecting to see more variation. The curious one is Åsa. That’s my first name! I’m guessing it was scraped from my website, perhaps from the title.

Passwords are a bit more interesting. There are 490 passwords that were used in the 8801 login attempts. My first name appears here again with passwords such as Åsa0, Åsa2 and Åsa123. Those were only attempted a few times though. The most common passwords by far were variants of the domain name minus top domain plus a few numbers (commonly a year such as 2005, 2010 or 2017). “123” takes second place as the most commonly occurring string and “admin” lands on third place. Let’s also give a honorary mention to passw0rd, secret, hunter and qwerty.


Summarizing now the takeaway would be something like this

  • It’s normal to have large amount of malicious login attempts on a WordPress website. If you install a security plugin and see lots of login attempts – don’t freak out. Those logins were always happening but you just didn’t see them before.
  • Login attempts in large numbers aka “brute force attacks” are generally predictable. There is very little variation in the usernames and the majority of passwords follow a common pattern.
  • Don’t use your domain name in your username or password. Also don’t use “admin” but hopefully you already knew this.
  • Be aware that your website can be scraped and that the data can used against you. It’s not common, but some bots are smarter than others.

Remember that this is a small dataset from one website with it’s own peculiarities. I block all xmlrpc.php requests. I’ve hidden my actual username. If you have a theme that exposes your WordPress username, or if you haven’t blocked username enumeration you’ll likely see a lot of login attempts with your actual username.

I’ll end with a word of caution of another kind. If this post inspired you to start logging things that’s great but be careful. You don’t want to accidentally log actual users passwords or any other private information.

That’s all for today. Happy coding! Have fun!

Wordfence Documentation

WordPress plugin with template and menu system for documentation. Json export function and invention of an “URL Mapper” that integrates with the Wordfence security plugin to make it easier to link from the plugin to the docs.

Year 2018
Produced by Defiant Inc
My contribution PHP, HTML, CSS, JavaScript

Renaming files can be a big mistake

Any project we are working on should always have some type of development environment where we can test code before we take it live (aka “in production”). In reality, this is not always the case due to resource or time constraints. When working on live sites we don’t want to make mistakes but unfortunately there is a huge mistake we can make while trying to avoid mistakes. This mistake is making backups of files and leaving those backups on the live server, accessible to the world.

But how would anyone know where to look for them? you might ask. Well, they can guess. Us humans are surprisingly predictable and assuming a pattern of behavior can obviously yield results. Otherwise the “bad guys” wouldn’t keep trying. I recently reviewed my access logs. Judging by the assumptions made by the hackers, these are the names that people have used in the past when making live backups of their WordPress config file (wp-config.php).

Examples of wp-config backup file scans:


Remember that only files ending in .php will be executed before they are served so the examples above deliver all your WordPress database information in plain text format to anyone who were to access those files. If you think you may have done this on a WordPress site in the past, you better undo it asap. If it was a long time ago, there is a big risk the site is now hacked. Ideally, you should not be leaving any backup files on the live site at all. It takes up space can cause confusion for other developers about which files are actually in use. If you have a good reason to leave backups of files on the live server, make sure you put them below the root (public_html/www) so that they are not accessible to the world.

I’ll add that whether database credentials can be used to compromise your site will depend on configurations on that specific server. If the server only allows database connections from localhost, database credentials alone are not enough to hack a site. Unless of course you’ve reused your database password (used the same password for your control panel or WordPress admin). Hopefully you’ve never done that. 🙂

Return 404 response code for certain query strings in WordPress

When WordPress site owners have been victims of hacking they often suffer consequences by getting blocked by Google or getting warnings from Google about malicious URLs on their site. After cleaning the site, these problems can linger when the URLs are only query strings and not actual URLs because query strings will not trigger a 404 in WordPress. One way of fixing this is to gather all the nasty query strings and then set them up to trigger a 404. Here is a basic script that does just that.

To add query strings to the list that triggers a 404, add them in the “force404” array. In the example below the following URLs force a 404.

Please note that this script requires a 404 template so make sure your theme has one.

add_filter('template_redirect', 'force_404_override' );
function force_404_override() {
	parse_str($_SERVER['QUERY_STRING'], $qs);

	$force404 = Array("some-spammy-query", "another-spammy-query");
	foreach ($qs as $key => $value) {
		if (in_array($key, $force404)) {
			status_header( 404 );
			include( get_query_template( '404' ) );