Over recent years Pagely’s security team noticed a trend in WordPress related attacks targeting unauthenticated changes to a WordPress website’s options table. The attack is specific to WordPress, but in its boiled down essence, this vulnerability would fall under Broken Access Controls/Elevation of Privilege (OWASP Top 10, 2017 A5). In laypersons terms: the application lacks proper authorization checks before performing a sensitive action.
Over the course of the year, reports of unauthenticated site option update vulnerabilities experienced a snowball effect. Initially, only a few reported vulnerabilities related to a WordPress site’s option table values being able to be updated without authentication went public. Some months later, more reports began to appear as more people started looking for and identifying this vulnerability. All the while attackers were targeting and infecting live sites. By summer, a cyber-arms-race was in full swing, the race was between malicious parties exploiting sites vs. defenders and researchers working to identify, patch, and protect websites.
You can see this trend as well as example vulnerabilities and their disclosure dates on WPVulnDB here.
What is an Unauthenticated Site Options Update vulnerability in WordPress exactly and how was it being targeted?
This vulnerability, as per its name occurs when a website’s wp_options value is allowed to be changed or updated by someone who is not logged in. Of course, it is not literally a form or button that is letting people change the site’s wp_option values, but it might as well have been (I will get into this later in the section for plugin developers.)
What is the risk associated with updating values in wp_options without authentication?
I would say this is a high severity risk. It only takes a single request against a vulnerable site for an attacker to be successful, no authentication is needed and the result is devastating to the site.
The wp_options table contains two critical values, home, and siteurl, which are used by WordPress core to ensure the visitor is visiting the right URL. This is done via an HTTP redirect which happens very early in the page load if WordPress detects your browser is not pointed to the right URL.
Any site owner who has changed their WordPress website’s domain name at some point likely had to update the values of these two options before their site could complete the migration to a new domain name.
Malicious parties have focused their attacks to alter the value of the home and siteurl options and are changing the value to their own domain names. This results in a destructive action, redirecting all of the infected site’s traffic to the attacker’s domain. This results in the infected site potentially getting blacklisted from search engines, exposing the site’s visitors to malware, not to mention the site will effectively get no visitors while infected.
What can you do if your site is affected and is redirecting to another domain?
For Pagely customers, we have been actively scanning for these attacks and notified any affected customer as well as performed a security clean up which fixes the affected site. This is all part of our security practices included in all services, PressArmor.
If you don’t host with Pagely, and your site has been hit by one of these attackers, here is what you can do:
You will need to change the siteurl and home option values back to their correct (original) setting. This needs to be done via phpMyAdmin, wp-cli, or a direct connection to the database server.
The easiest way to fix a site is via wp-cli:
Here are the two commands to run:
wp option update siteurl ‘http://yourwebsitehere’
wp option update home ‘http://yourwebsitehere’
That will update your website’s siteurl and home option values to their correct setting (of course, please use your actual domain name and not yourwebsitehere)
If you are not comfortable or do not have database or wp-cli access, but you can also address these hacks by editing your site’s wp-config.php file (via sFTP or such), which will work in a pinch. Here are the steps to follow.
- First edit your site’s wp-config.php file and define the following two WordPress constants ‘WP_HOME’ and ‘WP_SITEURL’ like so:
define( 'WP_HOME', 'http://yourwebsitehere' );
define( 'WP_SITEURL', 'http://yourwebsitehere' );
This hard codes these values into the site and you will not be able to easily change them in the future via the dashboard, so it is not a configuration you want to leave active even though your site will begin functioning normally again.
- Now, you can log in to your wp-admin dashboard and correct the modified values in your General Settings page. (You may need to clear browser and server caches.)
- Now, remove those define(‘WP_HOME’ … and define(‘WP_SITEURL… lines from your site’s wp-config.php file and test that your site is working as expected.
- Finally, you must remember to update the plugin with the vulnerability in it which exposed your site to this hack, if you miss this step you will likely get hacked again shortly and have to perform all of the above tasks all over again.
What can a plugin developer do to address this?
What went wrong?
Knowing how this exploit works is key in knowing how to code defensively and prevent your plugins from being vulnerable. Looking over the vulnerable plugins that were affected, the core issue was they used a high-risk function (update_option()) and forgot to include a step to verify the person making the request is authenticated and authorized to perform such an action like updating the site’s option values.
The exploits all follow this same trend:
The developer wants to update a site’s options, and to make it function smoothly on the site they make it an AJAX call via add_action() (in simpler terms, they designed the site to call the function that updates the site’s options via javascript, instead of via a form that would require full page loads). Within their site option updating function, they read data from the browser via GET/POST values to update the appropriate option setting.
The developer incorrectly believes there is nothing wrong with this idea, the code controls the GET/POST variables being sent to the AJAX endpoint and only they are making calls to the AJAX endpoint at properly authenticated places within their code. They assume they have written secure code by putting in some role validation before calling the AJAX request, they may even perform sanitization of the GET/POST values before the AJAX request is called… but they missed the critical flaw:
An AJAX endpoint created with add_action, is accessible from anyone on the internet to make a direct request to. To develop a secure AJAX endpoint, you must perform all security checks (sanitization, authentication, etc..) within the function add_action calls itself.
How can it be fixed?
When you create a new AJAX endpoint and point it to a function, you must remember that it exposes that function directly to the internet. If you take any sensitive actions within that function, then you must have appropriate checks within the function to ensure the user making the request has sufficient privileges to perform the sensitive action.
This is especially important for add_actions starting with wp_ajax_nopriv. The AJAX endpoints naming allows you to limit the endpoint to anyone on the web (intended for frontend AJAX requests) or only logged-in users of the site (intended for backend requests). Simply naming the endpoint ‘wp_ajax_[function_name]’ limits it to logged-in users, but you must be extra cautious with endpoints that are named ‘wp_ajax_nopriv_[function_name]’ as they will be available to everyone (no logged-in user required.) Most plugins vulnerable to this attack placed a call to update_option() within an AJAX endpoint named wp_ajax_nopriv.
I would recommend developers always perform authorization verification before taking any sensitive actions, but it’s especially true to exist within functions connected to AJAX endpoints. It is true that backend AJAX request (without nopriv) are only available to logged-in users, however this means they are accessible for admin users, as well as authors and subscribers, so you still need to check the user’s role and capabilities before taking any sensitive actions.
The function you should be using to check if a user can take an action is current_user_can() This function’s purpose is to check if the user’s role and capabilities allow for them to take any action, and there is a long list of capabilities you can check against. For the sake of this article’s topic, before you call update_option() you can check if the currently logged in user can manage site options via: current_user_can(‘manage_options’);
I have a quick gotcha! I need to address here too. In one of the affected plugins, the developer attempted to put in an authorization check but misunderstood the intention of the function is_admin(). Within their AJAX endpoint function, before any code is executed, the developer put a conditional that is_admin() must return true (otherwise exit immediately). The problem is, they were assuming this function returns true if the logged-in user is an admin user, and that would be wrong. is_admin() checks if the request URL appears to be from the wp-admin dashboard (e.g.. does it contain /wp-admin/?) which will return true for all WordPress AJAX requests, because, all AJAX requests are sent to /wp-admin/admin-ajax.php, the URL for AJAX endpoints always has the path to the wp-admin dashboard, is_admin() always returns true for AJAX requests.
I hope the information above is helpful for anyone running a site affected by this WordPress specific vulnerability, as well as for plugin developers who may want to double-check how they are doing proper authorization within their AJAX calls or before they call sensitive functions like update_option().