You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

207 lines
7.2 KiB

---
title: Enumerating Users on WordPress
description: |
Finding valid usernames can significantly improve your chances of breaking into a WordPress
account. In this blog post I cover some of the methods I use to find valid users and how you
can protect your own site against them.
date: 2020-09-05
tags:
- Security Advice
- Websites
---
WordPress is an extremely popular Content Management System (CMS) and as a result receives a lot of
interest from hackers. WordPress has a bad reputation in some circles for being insecure, however if
you are selective about the themes / plugins you install and keep it up to date, it is my belief
that it is a nice system for both developers and users.
However, assuming you keep everything up to date, in many cases the biggest security weakness is
your credentials. If a malicious actor guesses your username and password, it doesn't matter how
recently you did your updates, they are probably going to get in.
When I am tasked with testing the security of a WordPress site, one of the first things I do is
attempt to find usernames. In this blog post, I document some of the ways I do that.
:::note
For the purposes of this blog post, I have created a local WordPress site I can use for testing. Do
not attempt these tactics unless you own the site you are testing or have explicit permission from
the site owner to do so.
:::
## Trial and error
![Login form showing valid user](../../assets/wordpress/login-username-enumeration.png)
The most simple way is to attempt to login in with common usernames, `admin` being one of the most
common. You can see from the screenshots above that if you enter a correct username, the error
message tells you that you have the password wrong; if you enter an incorrect username, the message
tells you that there is an unknown username.
This makes it trivial to tell if a username exists: try it and if you get the "incorrect password"
error, you know you have a valid username.
To fix this, you simply need to make wordpress return generic error messages:
```php
<?php
function no_wordpress_errors(){
return 'Something is wrong!';
}
add_filter( 'login_errors', 'no_wordpress_errors' );
```
## User ID Cycling
Wordpress dynamically assigns users with IDs and creates pages for each user. Normally these can be
accessed by going to a url like `<domain>/author/admin/`. However, you can also access them by
manually specifying the users ID. For example `<domain>/?author=1` will redirect the visitor to
`<domain>/author/admin/`, this gives the attacker an easy way to get usernames.
```bash
for i in {1..5}; do
curl -s -o /dev/null -w "%{redirect_url}\n" "example-wordpress.local/?author=$i"
done
```
In a default WordPress installation, you will get something like this:
```
http://example-wordpress.local/author/admin/
http://example-wordpress.local/author/user1/
http://example-wordpress.local/author/user2/
http://example-wordpress.local/author/user3/
```
We have just found 3 new usernames.
One solution to this is to simply prevent WordPress queries from being able to look up a user by ID.
```php
<?php
function do_404_author_query($query_vars) {
if ( !empty($query_vars['author'])) {
global $wp_query;
$wp_query->set_404();
status_header(404);
nocache_headers();
$template = get_404_template();
if ($template && file_exists($template)) {
include($template);
}
exit;
}
return $query_vars;
}
add_action('request', 'do_404_author_query');
```
## Rest API
Out of the box, WordPress gives out a lot of information out about its users through the Rest API.
I can pull out a list of all usernames with the following:
```bash
curl -s example-wordpress.local/wp-json/wp/v2/users | jq '.[].name'
```
The easiest way to mitigate this is simply to remove the user endpoints.
```php
function remove_users_endpoints( $endpoints ) {
return array_filter( $endpoints, function($endpoint){
return (0 === preg_match( '/^\/wp\/v2\/users/', $endpoint ));
} , ARRAY_FILTER_USE_KEY);
}
add_filter( 'rest_endpoints', 'remove_users_endpoints' );
```
## oEmbed
Oembed is a protocol that allows websites to embed content from other sites. WordPress supports both
embedding and being embedded. When a site requests to embed a page, it makes a request that looks
like the following: `<domain>/wp-json/oembed/1.0/embed?url=http%3A%2F%2F<domain>/a-post-by-user-3/`.
The WordPress server then returns something like this:
```json
{
"version": "1.0",
"provider_name": "Example Wordpress",
"provider_url": "http://example-wordpress.local",
"author_name": "user3",
"author_url": "http://example-wordpress.local/author/user3/",
"title": "a post by user 3",
"type": "rich",
"width": 600,
"height": 338,
"html": "<embed code>"
}
```
This includes the author's name and a url for the author archive. In order to prevent wordpress from
including this information in the oembed response, add the following to a plugin or to your themes
functions.php file:
```php
<?php
function remove_author_from_oembed($data) {
unset($data['author_url']);
unset($data['author_name']);
return $data;
}
add_filter( 'oembed_response_data', 'remove_author_from_oembed' );
```
## In-Page Info
Perhaps the least interesting is simply looking at pages. Many pages, particularly news or blog
pages, include the author. This is often linked to the author archive page which will disclose their
username. It is normally possible to use simple tools like grep or hq to get the usernames from
these pages.
For example, to get the author of the page <http://example-wordpress.local/a-post-by-user-2/>, I
could do the following:
```bash
$ curl http://example-wordpress.local/a-post-by-user-2/ | hq '.post-author a' attr href'
http://example-wordpress.local/author/user2/
```
Here we see the author is `user2`.
However, sites often have hundreds or thousands of pages so doing this manually would be tedious.
Once again, we can turn to the rest api.
The following will go through the first 100 posts on the site and attempt to get the author's link
from it.
```bash
$ curl http://example-wordpress.local/wp-json/wp/v2/posts?per_page=100 | jq ‘.[].id’ | while read i; do
curl -L …/?p=$i | hq '.post-author a' attr href
done
```
It is worth noting here that the wordpress rest api limits requests to 100 results per request. This
means that if a site has more posts / pages than that, you might need to use the `page=n` parameter
which will give you the nth page of results.
I don't normally find this necessary since it is normally the earliest pages that are created by
high privileged accounts.
The easiest way to rectify this is simply not to include the author's details in the page, although
instructions on how to do this will vary depending on the theme in use. However, you may wish to be
able to group blog posts by author. In this case, I would suggest only publishing content using low
privileged accounts (author or contributor). This will mean that in the event that one of these
accounts is compromised, the damage an attacker can do is limited.
---
These steps should prevent most attempts at user enumeration although remember that security is not
a plugin or a few lines of code copied from a blog on the internet. There are many layers that
should be implemented and this is but one. For details on how best to secure your website, you
should consult with an expert.