A while ago, a WordPress site moved to us from another host. It arrived with its posts, its plugins, its theme and its users, the way every migrated site does. One of those users hadn’t belonged to anyone for years, and somebody out there still had its password.
We’ll walk through how we found it, what the people using it seemed to be after, and how it got through two changes of host unnoticed. We’ve left out the site’s address, its owner and every username. Where we show an example, we’ve made it up and we say so.
A number that didn't fit#
It showed up on the site’s Login activity tab, the page in the Hostney panel that lists every sign-in to a WordPress site. Most sites show a short, boring pattern there: a handful of people signing in from the same few places, and a long tail of failed guesses from bots.
On this one, Hostney login telemetry showed a single account with 509 successful sign-ins over about three months, and 1,855 failed attempts. That’s five or six successful sign-ins a day on average, from addresses that kept changing. They were Tor exit nodes, anonymous proxies and cloud servers.
People don’t usually sign in to their own site several times a day from a new address each time. It looked like somebody had a working password and was checking that it still worked.
The account nobody remembered#
The account belonged to a former member of the site owner’s team. It had been created in 2020, it had written real posts as recently as 2023, and at some point after that, someone removed its role. When a person leaves, taking away what their account can do looks like the right move.
WordPress still lets an account with no role sign in. The password is checked, the sign-in succeeds, the session cookie is issued. The account just can’t do much once it’s inside, because every permission check comes back empty.
So the login kept working. When the site moved to its second host, the account moved with it, and when it moved to us, the same thing happened again. WordPress keeps its users and their password hashes in the site’s database, and every migration copies the database, so the logins travel with it.
There was a second leftover, too. The site’s first host had created a support account of its own, with a registration date of 0000-00-00, and it had outlived two hosts the same way.
What they were doing with it#
In the site’s web server logs, the addresses that signed in to that account show the same shape over and over. A sign-in, then a short tour:
-
wp-admin, refused. No role, no dashboard. - The plugin pages, the plugin installer and the settings pages, all refused.
- WooCommerce’s order and report pages, refused. Somebody was hoping for a shop with customer data in it.
- The account’s own profile through the REST API, allowed. WordPress lets any signed-in user read and edit their own profile, role or not.
-
xmlrpc.php, used 231 times, plus another 130 attempts that our rate limiting turned away.
Then a different address, a few hours later, does the same thing. The clients were scripts, not browsers. One of them couldn’t even spell its own disguise. It told the server it was “Mozila/5.0”.
To us, this looks like credential checking. Somebody has a list of logins, from a breach, a phishing kit, a reused password, wherever, and they test each one regularly so they know which ones still work, and can sell them. They look around to see what each login can reach, because a login into a store’s admin is worth far more than one that can only edit its own bio.
At the time we looked, this one could reach almost nothing. That could change quickly. If someone gave the account a role again, or a plugin bug let any signed-in user do more than it should, the same password would open a lot more.
The same logs show requests for paths where other people’s backdoors usually live, scripts named like a forgotten admin tool or a fake WordPress file. Every one of them came back as not found.
How did the password get out?#
We don’t know. It could have come from a password reused somewhere else, an old laptop, or a phishing email years ago.
It’s also on the public record that the site started life on GoDaddy in 2020 and was still there in 2021, the year GoDaddy disclosed that an attacker had been inside its Managed WordPress provisioning system from September 6 until November 17. The original WordPress admin passwords set up for those sites were among what was exposed, and GoDaddy said it reset the affected passwords. We can’t connect any of that to this account.
Wherever it came from, it kept working through two moves, and it would have kept working until someone deleted the account or changed the password.
Was anything damaged?#
Not that we could find. We compared every WordPress core file and every plugin from wordpress.org with the files wordpress.org publishes for that exact version. Core matched. All 13 wordpress.org plugins matched. The only must-use plugins on the site were our own.
We did find one thing worth fixing that had nothing to do with the login: a custom plugin from 2020 that checks for its own updates over plain HTTP, at a bare IP address. If that address ever changes hands, whoever holds it can offer the site an “update”.
What this case changed at Hostney#
Some of what follows was already part of every WordPress site at Hostney. The rest exists because of this site. We found this login by reading logs by hand, and that shouldn’t depend on someone happening to look, so we built tools that look for you.
Every sign-in is recorded, with how it happened#
Login activity records every sign-in attempt: when, from where, and whether it worked. Because of this case, it now also records how someone signed in (the regular sign-in page, XML-RPC or an application password) and which roles the account had at that moment. An account with no role carries a “No role” badge on the page. That badge alone would have made this account stand out.
Sign-ins through XML-RPC didn’t show up on the page before. They do now.
Signals that email you#
Some sign-ins deserve an email the moment they happen, and this case is why these alerts exist. Hostney sends one to the account owner and to anyone they’ve given access to the site’s WordPress pages, when:
- an account with no role signs in
- a sign-in succeeds right after 10 or more failed attempts from the same address within an hour
- a sign-in comes from a Tor exit node, or from a network on a public blocklist of known attackers
- a sign-in comes from an address our bot protection already rates as malicious
- a new administrator appears, or an existing account is made one
The address and failure signals apply to accounts that can change things (administrators, editors, shop managers) and to accounts with no role, so a shop customer who mistypes their password doesn’t set off your inbox. Each email says what happened, where it came from and what to do if it wasn’t you, and they’re rate limited per site, so a busy attack doesn’t bury anyone.
With these in place, the owner of this site would have heard about the account the first time it signed in, instead of months later.
There are also optional emails for every administrator sign-in, or only when an administrator signs in from a country that account hasn’t used in the last 90 days. Both are off by default, one switch each on the site’s Security tab.
The Security tab closes the doors this login used#
The site’s Security tab has a switch for each of these, and every WordPress security email we send lists the ones that are still off:
- Disable XML-RPC stops password guessing and sign-ins through
xmlrpc.php. Skip it if you use Jetpack, the WordPress mobile app or another tool that connects that way. - Block REST API user enumeration and Block author enumeration stop people from reading your usernames off the site. This account’s username was public for a simple reason: it had published posts, and WordPress lists post authors through its API.
- SSO-only login turns off the WordPress sign-in page, so you sign in through the Hostney panel instead. Then a leaked WordPress password isn’t enough to sign in there. Pair it with Disable XML-RPC so the other way in is shut too.
- Block PHP execution in uploads and Disable file editor limit what a stolen login, or malware, can do if it ever gets further than this one did.
There’s more on what XML-RPC is and when you can switch it off, and on securing the WordPress login page in general.
The files are checked every night#
The file comparison we did by hand for this site is automatic now. It runs on every WordPress site at Hostney each night, at low priority so it stays out of the way. It covers core, plugins and themes, and it reads the files without running any of the site’s code. A premium plugin or theme, which has no published file list, is compared with copies of the same version on other sites we host. When a nightly check finds something the previous one didn’t, like a changed core file, a new PHP file in the uploads folder or a must-use plugin nobody installed, the owner gets an email.
It sits next to the vulnerability matching that tells you which plugins to update first and the bot protection that runs at the server, before a request ever reaches WordPress.
What to check after you move a WordPress site#
You don’t need to be on Hostney to do any of this. If you’ve moved a site, or taken over one that someone else moved, set aside fifteen minutes.
1. List every user and ask who each one is. In wp-admin, that’s the Users page. With WP-CLI:
wp user list --fields=ID,user_login,user_registered,roles
Look for accounts with an empty roles column, accounts nobody can put a name to, and anything with an impossible registration date. The output below is made up, but it shows the three kinds to look for:
ID user_login user_registered roles
1 sitename_admin 2020-09-15 10:02:11 administrator
6 former-staffer 2020-09-15 11:40:52
9 host-support 0000-00-00 00:00:00 administrator
2. Delete what nobody uses, and keep the content. Deleting a user lets you hand their posts to someone else, so nothing disappears:
wp user delete 6 --reassign=1
Check the ID twice before you run it. Our guide to WordPress user roles and user management covers what each role can do, if you’re deciding whether to keep an account.
3. Sign everyone out. Deleting an account ends its sessions. Shuffling the site’s salts ends every open session at once, so nobody stays signed in from before the cleanup:
wp config shuffle-salts
Everyone, you included, has to sign in again afterwards.
4. Remove the previous host’s leftovers. Managed hosts often create support accounts and must-use plugins of their own. Once you’ve moved, they’re just more ways in.
5. Turn off what you don’t use. XML-RPC, the public user list and the file editor are the usual three.
Most of the mistakes that make WordPress migrations go wrong break something on moving day, so they get fixed. An old login breaks nothing, so it’s easy to miss.
What this data can and can't tell us#
All of this comes from one site, so please don’t read it as a trend. We can only see what happened while the site was with us. The account had been signing in for a while before we noticed, and how long it had been doing that, including at the previous hosts, we can’t tell.
“Credential checking” is our reading of the pattern: rotating addresses, scripted clients, a tour of what the login can reach, no damage. It fits everything we saw, but we didn’t watch anyone sell anything.
And we can’t say where the password leaked. We’ve told you what’s on the public record about the site’s first host, and that we can’t connect the two.
If you’d like to compare notes, we’ve written before about what attackers type into WordPress login boxes, from 8,007 attempts in the same telemetry. If you’re moving a site to us, our guide to migrating a WordPress site to Hostney walks through it, and Login activity shows who’s signing in from the first day.
The site, its owner and every username in this post have been left out. The WP-CLI output above is a made-up example.