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