There are many guides out there that explain how to secure a WordPress installation. This article is not yet another one of them. We will look at how a typical WordPress can be compromised en mass. The steps of this attack are derived from reversing actual breached websites and are applicable to versions as recent as 4.7 and require no additional plugins.
The purpose of this article is to illustrate how various vulnerabilities in WordPress are pieced together and that WordPress is, by design, non-secure.
The gist of the attack is quite straightforward - we guess the administrator's user name and brute-force the password.
Easier said than done, you say. The default power user is "admin" but it is often not the case. There are billions of possible password combinations; trying all of them could take decades. Well, let's see how WordPress makes the attack that much easier.
User Enumeration
Out of the box, WordPress creates an admin user. The name of this user is not known, but typically it has an ID of 1. In case the initial user was replaced by another user, we can scan the range of 1-10, which takes very little time to perform.
In a default template, there is a link to the author, where all the posts that are published by the author is listed. The link to the author often uses a friendly format. For example:
http://target-site.com/author/not-admin/
We can see that the user name is set to "not-admin" in an attempt to evade brute-force sign-on. The website may also remove all the links to authors. This is especially true with non-blog web-sites that use "posts" and "pages" to emulate content pages.
WordPress has flexible (read: open to exploit) routing. For example, regardless whether there is a search box, one can always append ?s= on any page link to trigger a search. Similarly, we can find out the name of any user by its ID:
http://target-site.com/?author=1
If the author exists, WordPress rewrites the URL to the following:
http://target-site.com/author/not-admin/
The "proper name" of the user is displayed in the page title. If the URL is not rewritten, the user cannot publish posts. If the result is a 404 page, the user record does not exist. Many WordPress web sites have just a handful of users. We can quickly determine the total number of users and which ones can publish, which in turn are likely admin users. Alternatively we could also find out whether a user is admin in the brute-force step, but scanning numeric range is a lot quicker.
A sane system hides usernames whereas WordPress simply gives them away!
Brute-force Passwords
The two most popular attack vectors against WordPress are wp-cron and xml-rpc. They are both enabled by the default. Here we show how xml-rpc is used.
Armed with a username that we know to be admin (see Step 1) and a brute-force dictionary, we post the following request to xml-rpc.php:
<?xml version="1.0" encoding="iso-8859-1"?>
<methodCall>
<methodName>wp.getUsersBlogs</methodName>
<params>
<param>
<value>
<string>admin</string>
</value>
</param>
<param>
<value>
<string>password123</string>
</value>
</param>
</params>
</methodCall>
For a thousand guesses we need to make a thousand requests. WordPress, again, makes this process a lot easier. Meet system.multicall:
<?xml version="1.0"?>
<methodCall>
<methodName>system.multicall</methodName>
<params>
<param><value><array><data>
<value><struct>
<member>
<name>methodName</name>
<value><string>wp.getUsersBlogs</string></value>
</member>
<member>
<name>params</name><value><array><data>
<value><array><data>
<value><string>user1</string></value>
<value><string>password1</string></value>
</data></array></value>
</data></array></value>
</member>
</struct></value>
<value><struct>
<member>
<name>methodName</name>
<value><string>wp.getUsersBlogs</string></value>
</member>
<member>
<name>params</name>
<value><array><data>
<value><array><data>
<value><string>user2</string></value>
<value><string>password2</string></value>
</data></array></value>
</data></array></value>
</member>
</struct></value>
</data></array></value>
</param>
</params>
</methodCall>
The above request tries two user-password pairs at once. We can pack hundreds or even thousands of passwords in a single request. We can also split millions of guesses over hundreds of requests and spread the call over time.
Now, just how realistic is it to brute-force passwords? Well, people had much success in the 90s but things have changed... for worse: more users are authoring content on the web, which means higher chance of someone using lazy passwords. Many brute-force routines combine usernames and passwords. In this case, we know the username, so half of the battle is already won. Internet latency would have been an issue if were not for the multicall in xml-rpc.
Although machines' ability of generating cryptographic tokens have advanced, us humans haven't changed much. Passwords could be encrypted, hashed and stored securely in the database. But if the passwords are weak and that trying them is so trivial, the human behavior becomes the weakest link.
The chance of your password being in a brute-force dictionary is higher than you think. The chance that a weak password being used among one of your company staff is even higher. Many passwords are combinations of lower-case alphanumeric letters. More specifically, just a few letters followed by just a few numbers or vice-versa.
In addition to trying passwords, xml-rpc's multicall also makes it possible to publish posts en mass. At this point, the attacker's imagination is the only boundary of the target's damage.
Gyroscope, on the other hand...
Weak password is a problem that plagues all systems. Forcing the users to mix in symbols and letter casing is just annoying and not all that effective. Gyroscope addresses the issue in a number of ways:
As a starter, it is a "sane system". It does not disclose its users or allow batched password attempts. Gyroscope also has configurable endpoints so that its location (the counter part of wp-admin) and routing page (the counter part of ajax-admin.php) can be changed at will.
Furthermore, chipped smart cards and key files can be used in addition to just passwords.
Starting v12.0, Gyroscope users can generate random key files. As the user tries to catch a randomly positioned box, his mouse movement and timing are captured. This, in combination with server-side keys and other random or user-specific factors, a unique key file is created.