|
|
|
---
|
|
|
|
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
|
|
|
|
- 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.
|
|
|
|
|
|
|
|
|