As of May 2019, WordPress is used by 33.8% of all websites. This equals to 60.6% of all websites with known CMS versions. Over 50% of these sites use outdated and insecure versions of WordPress. In this series, we are going to examine how hackers attack WordPress and how WordPress can be protected from these attacks.


More than 90% of daily attacks are carried out by automated scripts, so-called bots. Millions of these bots scan the Internet at any moment. It's more a matter of seconds than minutes until they find a vulnerable WordPress installation.

ENABLE AUTOUPDATE

The most important security measure is to keep WordPress up to date, including all plugins. Most new WordPress versions fix more or less dangerous security vulnerabilities. These vulnerabilities are often exploited in the mass hours following the release of the patches. It is therefore very important to keep everything up to date. Fortunately, WordPress includes an autoupdate feature. This functionality has been available since WordPress 3.7. Older versions must first be updated manually using the “Update” button. To enable an automatic update, all we need to do is insert the following code at the end of our wp-config.php file. It is usually located in the WordPress root directory.

add_filter( 'auto_update_core', '__return_true' );    // Enable updates for wordpress
add_filter( 'auto_update_plugin', '__return_true' );  // Enable updates for all plugins
add_filter( 'auto_update_theme', '__return_true' );   // Enable updates for all themes
add_filter( 'auto_update_translation', '__return_true' ); // Enable translation updates
To make the website as secure as possible, we need to disable all plugins that we don’t necessarily need, and then uninstall them. If we have any plugins that are no longer maintained, we need to uninstall them. In addition to increased security, this can also improve the loading times of our pages.

WORDPRESS INFORMATION DISCLOUSURE

The first step in hacking a WordPress page is to get as much information as possible about it. Of course there are automated scripts that can do this for us. But we need to develop a basic understanding of how they work, otherwise we can’t really protect ourselves from attacks. Security is an ongoing process and not a one-time thing.
We are looking at a freshly installed WordPress running on our Ubuntu 18.04.2 LTS with the ip 192.168.50.1.
First we try to find the version of WordPress. If we know the version, we can search for vulnerabilities. Many bots just don’t care. They try the exploit and see if it works. But the scripts are getting more and more advanced. They try to hide their attacks and only attack vulnerable versions. Just like a human attacker would do.
We are using the tool netcat nc to generate the requests to out website.

1. FIND OUT WORDPRESS VERSION IN POST AND COMMENT FEEDS

We have found the tag for version 5.1.1..

echo "GET http://192.168.50.1/feed/" | nc 192.168.50.1 80 | grep generator
   <generator>https://wordpress.org/?v=5.1.1</generator>
echo "GET http://192.168.50.1/comments/feed/" | nc 192.168.50.1 80 | grep generator
    <generator>https://wordpress.org/?v=5.1.1</generator>

2. READ WORDPRESS VERSION VIA GENERATOR-TAG

Again, version 5.1.1.

echo "GET http://192.168.50.1/" | nc 192.168.50.1 80 | grep generator
    <meta name="generator" content="WordPress 5.1.1" />

3. FIND OUT WORDPRESS VERSION IN CSS- AND JS-INCLUDES

Looking at the page sourcecode we see multible stylesheet and JavaScript includes.

<link rel='stylesheet' href='/wp-includes/css/style.css?ver=5.1.1' type='text/css' media='all' />

The includes are tagged with the WordPress version.
echo "GET http://192.168.50.1/" | nc 192.168.50.1 80 | grep  -i -o -E "(\.css\?ver=.*)" | \
grep  -i -o -E "[1-9]{1}\.[0-9]{1,2}\.[0-9]{1,3}"
    5.1.1

4. READ WORDPRESS VERSION IN INSTALL.PHP AND UPGRADE.PHP

Inside the files wp-admin/install.php and wp-admin/upgrade.php are also version tags.

echo "GET http://192.168.50.1/wp-admin/install.php" | nc 192.168.50.1 80 | \
grep  -i -o -E "(\.css\?ver=.*)" | \
grep  -i -o -E "[1-9]{1}\.[0-9]{1,2}\.[0-9]{1,3}"
    5.1.1

   
   
echo "GET http://192.168.50.1/wp-admin/update.php" | nc 192.168.50.1 80 | \
grep  -i -o -E "(\.css\?ver=.*)" | \
grep  -i -o -E "[1-9]{1}\.[0-9]{1,2}\.[0-9]{1,3}"
    5.1.1    

The next information WordPress reveals are the login names or the names of the authors. Once we have the login names we only need the password to take control of the site. Many people use the same usernames and passwords for all pages. This is very problematic because of the many data breaches in recent months.
To get the author names, we just have to iterate over the following URL and increase the last digit.

curl -L -s http://192.168.50.1/?author=1 | grep author | \
perl -ne 'print "$2\n" if /(Posts by )(.*)( Feed\")/' 
    secretadmin
curl -L -s http://192.168.50.1/?author=2 | grep author | \
perl -ne 'print "$2\n" if /(Posts by )(.*)( Feed\")/' 
    John Doe

6. FIND WORDPRESS LOGINS VIA WORDPRESS REST-API

Since Version 4.7 WordPress provides usefull REST API which is a good source of information for an attacker. Some API functions are even available since WordPress 4.4. The API is pretty simple to use. This endpoint lists all users with at least one post.

curl -s http://192.168.50.1//wp-json/wp/v2/users | jq 
    [
      {
        "id": 1,
        "name": "secretadmin",
      },
      {
        "id": 2,
        "name": "John Doe",
      }
    ]
We use curl as the easiest way to read the data from the API.

PROTECTION AGAINST WORDPRESS INFORMATION DISCLOSURE

To solve the problems 1-3 and 5 we need to to extend our themes function.php. We can either modify the file directly, use a child theme or create our own functions-security.php and include it in the themes function.php file.

In this case we create a new file named functions-security.php in the theme-folder (in our case under /var/www/wordpress/wp-content/themes/twentyseventeen.

<?php


/* functions-security.php
 *
 * SECURITY SETTINGS
 *
 * include this with the following line in your theme function.php
 * require_once( 'functions-security.php' );
*/


// ------------------------------------------------------------------
// Supresss version tag scripts and styles

function remove_wp_tag_cssjs( $src ) {
    if ( strpos( $src, 'ver=' ) )
      $src = remove_query_arg( 'ver', $src );

    return $src;
}
add_filter( 'style_loader_src', 'remove_wp_tag_cssjs', 9999 );
add_filter( 'script_loader_src', 'remove_wp_tag_cssjs', 9999 );


// ------------------------------------------------------------------
// Hide wordpress generator  tag

function my_remove_version_info() {
    return '';
}
if (!is_admin()) {
        add_filter('the_generator', 'my_remove_version_info');
}


// ------------------------------------------------------------------
// Stop the user enumeration via links and author archives

if (!is_admin()) {
        if (preg_match('/author=([0-9]*)/i', $_SERVER['QUERY_STRING'])) die();
        add_filter('redirect_canonical', 'shapeSpace_check_enum', 10, 2);
}
function shapeSpace_check_enum($redirect, $request) {
        // permalink URL format
        if (preg_match('/\?author=([0-9]*)(\/*)/i', $request)) die();
        else return $redirect;
}


// ------------------------------------------------------------------
// Stop the user enumeration through the REST API

add_filter( 'rest_endpoints', function( $endpoints ){
    if ( isset( $endpoints['/wp/v2/users'] ) ) {
        unset( $endpoints['/wp/v2/users'] );
    }
    if ( isset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] ) ) {
        unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
    }
    return $endpoints;
});


// ------------------------------------------------------------------
// Disable XMLRPC
if  (!is_admin()) {
    add_filter( 'xmlrpc_enabled', '__return_false' );
}


?>

Add the following code as the last line to the actives themes functions.php in same folder.
require_once( 'functions-security.php' );

To solve finding number 4, a small change in the web server configuration is required.

For nginx we need to change the appropiate configuration in /etc/nginx/sites-available/YOURSITE.
We add the following code inside the server-directive.
location ~ wp-admin/(install(-helper)?|upgrade)\.php {
    rewrite ^(.*)$ / redirect;
}

For apache we create or modify a file named .htaccess in the webroot with the following code.
RedirectMatch Permanent wp-admin/install(-helper)?\.php /
RedirectMatch Permanent wp-admin/upgrade?\.php /

Restart your web server to activate the changes.

CONCLUSION

After activating autoupdates, deleting obsolete plugins and protecting against information disclosure, we have completed the basic protection of our WordPress page. There are still some attack techniques from which we have to protect WordPress. We will talk about this in the second part of this series.