207 lines
7.2 KiB
Markdown
207 lines
7.2 KiB
Markdown
---
|
||
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
|
||
|
||

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