Forgot to push for ages
This commit is contained in:
parent
e17a042f52
commit
9d8c010bad
25 changed files with 2069 additions and 18 deletions
207
content/blog/010-wordpress-username-enumeration.md
Normal file
207
content/blog/010-wordpress-username-enumeration.md
Normal file
|
@ -0,0 +1,207 @@
|
|||
---
|
||||
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
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue