20170310 WordPress Vulnerable Plugins/Themes Report

You should have already noticed that WordPress released version 4.7.3 update on Monday.  4.7.3 addresses six security vulnerabilities (one of which was discovered by my buddy Delta!) in addition to 39 bug fixes.  Equally important is that security patches were issued for all WordPress branches back to version 3.7.  If you are running any version of WordPress from 3.7 forward you should update immediately as there are now attacks in the wild targeting the vulnerabilities that were corrected.

If you are on an older version, please strongly consider upgrading to a more current branch. While I applaud the WordPress team for patching all branches back to 3.7, you can’t rely on them supporting those older branches moving forward.   Staying up-to-date is one of the most important ways to protect your site.  The WordPress development team has done an amazing job of ensuring backwards compatibility, so unless you have made changes to the core WordPress files, there is a strong chance you can update to the latest version without incident.  If you’re unsure of updating, please reach out to me and let’s see if we can get you updated.

I had hoped this week’s report would be quieter, but instead includes five unauthenticated arbitrary file upload disclosures.  PLEASE, remove or update these plugins immediately.

This week’s report.

WordPress 4.7.3 update released, WordCamp St. Louis, and WordCamp Miami!

WordPress update 4.7.3 was released earlier today.  4.7.3 is a maintenance and security update and corrects six security issues.  Let’s just hope there aren’t any undisclosed security patches and the 4.7.3 update is much quieter than the 4.7.2 update.

If you don’t follow me on twitter, you might have missed that I’m speaking at both WordCamp St. Louis on March 18th and at WordCamp Miami on March 25th. If you’re going to be at either event, definitely stop me and introduce yourself!

For both events, tickets are limited, so make sure you reserve your spot now!

20170302 Vulnerable Plugins/Themes Report

This week’s report is pretty large, due in large part to the disclosure of the remaining discoveries from last year’s sumofpwn that were never fixed, despite repeated attempts to contact/work with the developers.

There are a couple of items in the report I want to address directly.  They are listed in the notes section but I want to highlight them. In looking at the svn repository for Adminer, they fixed the issue in v1.4.5, but the plugin has been removed from the public repository. In general, having a world-accessible direct connection to your database is a bad idea. I would suggest going ahead and removing the plugin if you have it installed. You can read more about the initial disclosure.

The disclosure for FormBuilder was for version was 1.0.5 with the latest version being 1.0.8. Though the initial disclosure doesn’t mention it, the plugin does output the contents of user supplied data in other areas (and continues to do so in the most recent version). In addition, the plugin’s description mentions that the plugin is reaching end of life.

Be advised, FormBuilder is nearing end-of-life and may not be actively maintained in the future. It is advisable to switch your WordPress site to some other Form handling plugin at this time.

Given it continues to have potential issues and it’s reaching end-of-life, I would strongly suggest removing.

In regards to the Trust Form plugin, there is a version in the svn repository (2.0.1) where it appears the author tried to address some of the disclosed vulnerabilities. However, there are other areas that are still vulnerable to cross-site scripting, which is most likely why the plugin has been removed from the public repository. I would strongly suggest removing the plugin.

This week’s report.

Cloudflare breach and WordPress 4.7.3 release + Weekly Plugin/Theme vulnerabilities report

TL;DR – If you use cloudflare you need to invalidate all sessions for the site and update passwords for accounts immediately.

From TechCrunch:

Cloudflare revealed a serious bug in its software today that caused sensitive data like passwords, cookies, authentication tokens to spill in plaintext from its customers’ websites.

And from the initial disclosure:

if an html page hosted behind cloudflare had a specific combination of unbalanced tags, the proxy would intersperse pages of uninitialized memory into the output

Worse yet, some of that disclosed information was cached by search engines.  If you are using WordPress, and are using Cloudflare, you should change the salts in your wp-config.php file and update your passwords.   If you can’t have every registered user of your site update their passwords, then at a minimum, update all of the passwords for your administrator accounts.

For other sites that are using Cloudflare, you need to invalidate all sessions and have users update their passwords.

In WordPress specific news, the 4.7.3 update has been scheduled for release Monday, March 6th.  If you don’t have auto-updates enabled, make sure you plan to update your site.  As far as I know, this update mostly addresses bug fixes, but I haven’t had a chance to look over the full set of changes. 

And finally, this week’s vulnerable plugins/themes report

Multiple Independent Subdomains in a WordPress Network

TL;DR – You’re going to need to map your domains to the COOKIE_DOMAIN constant.

We run numerous WordPress networks on campus, almost all of which are set up with multiple independent subdomains (e.g. foo.missouri.edu, bar.missouri.edu, etc.).  Historically, a WordPress network only supported subdomains based off a root domain (e.g. root domain of mysite.com, child sites of foo.mysite.com, bar.mysite.com, etc.). One way to be able to map independent domains in a network was with the WordPress MU Domain Mapping plugin.  We’ve been doing this awhile so this was our standard set up when running a network.

Recently, Aaron Duke asked me why I was still using the MU Domain Mapping plugin when WordPress had integrated domain mapping natively into WordPress as of version 4.5. Short answer: I must have missed that in the changelog for 4.5.  Reading through the documentation, I still wasn’t convinced it would work in our situation (as described above).  I set up a test WordPress subdomain network for an account where I had two domains pointed to the same account on the server.   Following the directions from the docs¹, I set up both sites and added a second user. Added a couple of posts to the main site (foo.missouri.edu) and verified pretty links were working correctly. Good so far.  Went to log into the second site (bar.missouri.edu) and was presented with the “Cookies are blocked” error message.

Firefox warning that cookies are blocked in the browser

In looking at the headers, sure enough, WordPress was attempting to set the domain property of the WP Cookie Check cookie to foo.missouri.edu instead of bar.missouri.edu.  I reached out to some WordPress colleagues to try and see if it was something I had missed. The ever lovely Rachel Carden mentioned seeing similar issues if the DOMAIN_CURRENT_SITE constant was set incorrectly.  Charles Fulton suggested manually setting the COOKIE_DOMAIN constant to .missouri.edu.  While this would technically solve the issue, I didn’t want to wildcard the cookie like that as it would allow it to be read by a rogue site on the missouri.edu domain (and trust me, there are rogue sites out there).  Surely there had to be a better way.

Taking Rachel’s and Charles’ suggestions, I started digging into the WordPress core.  Since the problem is with the cookie domain, I started with debugging when and where WordPress sets cookies.  In wp-login.php,  we find that it sets the WP Cookie Check cookie at line 407, using the constant COOKIE_DOMAIN for the domain property.

setcookie( TEST_COOKIE, 'WP Cookie check', 0, COOKIEPATH, COOKIE_DOMAIN, $secure );

For subdomain multisites, this constant is defined starting at line 78 in the function ms_cookie_constants() in the file ms-default-constants.php.

function ms_cookie_constants(  ) {
   $current_network = get_network();

   /**
    * @since 1.2.0
    */
   if ( !defined( 'COOKIEPATH' ) )
      define( 'COOKIEPATH', $current_network->path );

   /**
    * @since 1.5.0
    */
   if ( !defined( 'SITECOOKIEPATH' ) )
      define( 'SITECOOKIEPATH', $current_network->path );

   /**
    * @since 2.6.0
    */
   if ( !defined( 'ADMIN_COOKIE_PATH' ) ) {
      if ( ! is_subdomain_install() || trim( parse_url( get_option( 'siteurl' ), PHP_URL_PATH ), '/' ) ) {
         define( 'ADMIN_COOKIE_PATH', SITECOOKIEPATH );
      } else {
         define( 'ADMIN_COOKIE_PATH', SITECOOKIEPATH . 'wp-admin' );
      }
   }

   /**
    * @since 2.0.0
    */
   if ( !defined('COOKIE_DOMAIN') && is_subdomain_install() ) {
      if ( !empty( $current_network->cookie_domain ) )
         define('COOKIE_DOMAIN', '.' . $current_network->cookie_domain);
      else
         define('COOKIE_DOMAIN', '.' . $current_network->domain);
   }
}

From there we see that it using the object $current_network for the value used to define the constant.  $current_network is returned from the call to function get_network at line 50.  get_network() is defined on line 1098 of ms-blogs.php and attempts to return an instance of WP_Network based on either a network ID passed into the function or by using the global $current_site.

function get_network( $network = null ) {
   global $current_site;
   if ( empty( $network ) && isset( $current_site ) ) {
      $network = $current_site;
   }

   if ( $network instanceof WP_Network ) {
      $_network = $network;
   } elseif ( is_object( $network ) ) {
      $_network = new WP_Network( $network );
   } else {
      $_network = WP_Network::get_instance( $network );
   }

   if ( ! $_network ) {
      return null;
   }

   /**
    * Fires after a network is retrieved.
    *
    * @since 4.6.0
    *
    * @param WP_Network $_network Network data.
    */
   $_network = apply_filters( 'get_network', $_network );

   return $_network;
}

We know our call to the functions from inside of ms_cookie_constants didn’t include any parameters, so the global $current_site must already contain data. Adding a watch revealed that was in fact already populated by the time get_network was called. Alright, we’ll need to track down when $current_site is initially created: schema.php, ms-settings.php, and ms-load.php.  ms-settings.php and ms-load.php are loaded during a login, and ms-settings.php calls the function ms_load_current_site_and_network() in ms-load.php which is where, FINALLY, we find the first instantiation of $current_site. Wait, why was I trying to track down $current_site?  😀

function ms_load_current_site_and_network( $domain, $path, $subdomain = false ) {
	global $wpdb, $current_site, $current_blog;

	// If the network is defined in wp-config.php, we can simply use that.
	if ( defined( 'DOMAIN_CURRENT_SITE' ) && defined( 'PATH_CURRENT_SITE' ) ) {
		$current_site = new stdClass;
		$current_site->id = defined( 'SITE_ID_CURRENT_SITE' ) ? SITE_ID_CURRENT_SITE : 1;
		$current_site->domain = DOMAIN_CURRENT_SITE;
		$current_site->path = PATH_CURRENT_SITE;
		if ( defined( 'BLOG_ID_CURRENT_SITE' ) ) {
			$current_site->blog_id = BLOG_ID_CURRENT_SITE;
		} elseif ( defined( 'BLOGID_CURRENT_SITE' ) ) { // deprecated.
			$current_site->blog_id = BLOGID_CURRENT_SITE;
		}

		if ( 0 === strcasecmp( $current_site->domain, $domain ) && 0 === strcasecmp( $current_site->path, $path ) ) {
			$current_blog = get_site_by_path( $domain, $path );
		} elseif ( '/' !== $current_site->path && 0 === strcasecmp( $current_site->domain, $domain ) && 0 === stripos( $path, $current_site->path ) ) {
			// If the current network has a path and also matches the domain and path of the request,
			// we need to look for a site using the first path segment following the network's path.
			$current_blog = get_site_by_path( $domain, $path, 1 + count( explode( '/', trim( $current_site->path, '/' ) ) ) );
		} else {
			// Otherwise, use the first path segment (as usual).
			$current_blog = get_site_by_path( $domain, $path, 1 );
		}

$current_site is simply a stdClass object where the property domain is set to the constant DOMAIN_CURRENT_SITE.  So now we know that back in the function get_network() in ms-blogs.php uses the value of $current_site for $network and uses it to instantiate a new instance of WP_Network at line 1107.  Now that we know where the information for WP_Network comes from, let’s look to see how it determines the property cookie_domain.  During the __construct method there is a call to the private method _set_cookie_domain(), and look there at line 244.

/**
 * Set the cookie domain based on the network domain if one has
 * not been populated.
 *
 * @todo What if the domain of the network doesn't match the current site?
 *
 * @since 4.4.0
 * @access private
 */
private function _set_cookie_domain() {
   if ( ! empty( $this->cookie_domain ) ) {
      return;
   }

   $this->cookie_domain = $this->domain;
   if ( 'www.' === substr( $this->cookie_domain, 0, 4 ) ) {
      $this->cookie_domain = substr( $this->cookie_domain, 4 );
   }
}

We finally see that cookie_domain is simply set to the same domain as was defined in the constant DOMAIN_CURRENT_SITE. And it appears that this scenario is a known issue; look at the todo there at line 234.

@todo What if the domain of the network doesn’t match the current site?

So now, circling all the way back to where we started, we can see that in the function ms_cookie_constants() in the file ms-default-constants.php, if COOKIE_DOMAIN isn’t already defined (e.g. in wp-config.php or a plugin), then it will be set to the cookie_domain or the domain properties, which are the exact same thing.  Which means that in order to set the domain property in the cookie to the correct domain in our WordPress network setup, we’re going to need to set it ourselves.

The challenge is we need to set the COOKIE_DOMAIN before wp-settings.php calls the function ms_cookie_constants().  In looking through the WordPress Core Load chart, I can see the only file we have direct access to before wp-settings.php loads is wp-config.php.  I could add our logic into wp-config.php, but I’d rather not place logic in a config file.  In looking through wp-settings.php, both network-activated plugins and must-user plugins are loaded and the action hook muplugins_loaded is fired before ms_cookie_constants is called.  There’s our answer: add the code to a plugin, hook it to the muplugins_loaded action and network-activate it!

The code is simple enough

Plugin Name: COOKIE_DOMAIN mapper
Plugin URI: http://gilzow.com/
Description: Maps the current domain into COOKIE_DOMAIN in a multisite set up
Author: @gilzow
Version: 0.1
Author URI: http://gilzow.com/
 */

add_action('muplugins_loaded',function(){
    if(defined('MULTISITE') && MULTISITE && !defined('COOKIE_DOMAIN') && DOMAIN_CURRENT_SITE !== $_SERVER['SERVER_NAME']){
        if(1 === preg_match('/^(?:www.)?((?:[A-Za-z0-9_\-]+\.){1,3}[A-Za-z0-9_\-]{2,})$/',$_SERVER['SERVER_NAME'],$aryMatches)){
            define('COOKIE_DOMAIN',$aryMatches[1]);
        }
    }
});

If MULTISITE is set to true, COOKIE_DOMAIN isn’t already defined and the SERVER_NAME environmental variable is not equal to the value in DOMAIN_CURRENT_SITE, send it to the regular expression (regex) statement.  In the regex, we start with checking to see if there is a www at the beginning, but we don’t care about it (the ?: indicates to the regex engine to not capture this group).  Then we check to see if there is 1 to 3 groups of 1 to infinity number of A through Z (lower and uppper case), digits, underscores and dashes followed by a period.  Those groups need to be followed a group of characters of at least 2 characters or more of the same A-Z (upper/lower), digits, underscores and dashes.  That whole thing is captured. If there’s a match, place the values matched and captured into the variable $aryMatches. If we have a match, take the capture group (key 1 in the array) and use that value for our COOKIE_DOMAIN constant.

Why the regex instead of just using SERVER_NAME or HTTP_HOST?  Host is from the header sent to us by the client so it can’t be trusted.  And while one would think SERVER_NAME would be safe since it is (supposed to be) an environmental variable, it’s actually determined in part from the HTTP_HOST value.  Now, it’s unlikely someone would be able to inject a value into the Host that would result in an exploit here, but better safe than sorry.  Instead, we’re going to simply make sure that the host name only includes alphanumeric characters, dashes and underscores.  The only thing that one might need to change is the 3 in {1,3}. At three, the domain foo.bar.gilzow.com (fourth-level) would be valid and match, but biz.foo.bar.gilzow.com (fifth-level) would not.  If you need to use fifth-level or more subdomains, you’ll need to adjust that value accordingly.

Having added the plugin, and network activating it, I can now successfully log into the subsite, and in viewing the headers, see that it is setting the domain property on the cookie to the correct domain.  Success!

Having walked through the code, I’m now wondering how anyone using independent domains has been able to get a WordPress subdomain network working correctly without accounting for the cookie domain property?  And given the @todo in the WP_Network class, it appears that this is a known limitation.  Or maybe our environment is unique and we’re the only ones running into this issue?

Hit me up in the WPCampus slack team or over at wordpress.org if you’d like to discuss it.

Note ¹: Our sites are located on a central shared-hosting, load-balanced environment.  Each site is a CNAME of the domain for the hosting environment which is set as the A Record.  So we were unable to follow Step #3 of Multisite Domain Mapping Without a Dedicated IP or Plugin since you’re not supposed to assign a CNAME to another CNAME. In addition, we do not map the subsite domain as a fourth-level subdomain of the main site’s domain.

20170210 Vulnerable Plugins/Themes Report

I know I sound like a broken record, but if you are running WordPress version 4.7.X please make sure you have installed the 4.7.2 update. There are now up to 1.5 million defacements due to the REST API vulnerability, and several monitoring companies are beginning to see Remote Code Execution attempts against the vulnerability.  If for any reason, you are on 4.7.0 or 4.7.1 and are unable to upgrade, or don’t think you can, PLEASE reach out to me. 

 This week’s report: https://docs.google.com/spreadsheets/d/1JaYqIIAclJjIQ3vWOmcLel5Bra0vPrRacVR8tt7H4uA/edit?usp=sharing

Why the WordPress REST API user endpoint still isn’t fixed and 20170113 Vulnerability Report

Not as many vulnerabilities to report this week (that’s good, right?).  Just four.

20170113 Vulnerable Plugins Report

I would like to mention that one of the security items fixed in version 4.7.1 of WordPress this week isn’t as complete as it initially sounds.

The REST API exposed user data for all users who had authored a post of a public post type. WordPress 4.7.1 limits this to only post types which have specified that they should be shown within the REST API.

As I have mentioned a couple of times, exposing your user data publicly is a bad idea and goes directly against OWASP A6 Sensitive Data Exposure.  From the changelog (quoted above) it sounds like in v4.7 your user data was only exposed if the user had authored a public post, but that’s not correct.  When I first heard about the user endpoints in the REST API, I discovered that all users who were capable of publishing were exposed, even if they had never published anything (and don’t forget: your first admin user is automatically added as the author of the example post when installing WordPress). In the v4.7.1 fix, they’ve changed that to be only post types that are to be shown in the REST API, but don’t forget that the default post type is included automatically.

WordPress User details
a WordPress Editor who has never published a post

The above screenshot is from a version 4.7.1 WordPress site that has not had the user endpoints removed. As you can see, the account adamsmel does not have any posts.  In fact, this particular site is brand new and doesn’t have any posts, published or draft, at all.   However, when querying the REST API for users, her account still shows up.

User account still shows up in the return from the REST API user endpoint

 

 

 

 

 

Now, it’s possible that the change introduced in version 4.7.1 only affects new user accounts that are added after the 4.7.1 update is applied, but that still leaves millions of sites at risk of exposing their usernames.

I love the REST API; I truly do, but considering the amount of information potentially exposed, it has to be done securely.  Until the REST API can be placed behind authentication, then the WordPress core team needs to remove the default post type from automatically being included in the REST API and remove the user endpoints.  Give developers the ability to expose those endpoints if they want, but don’t make it the default for the millions of WordPress installations that will never see a developer.

20170104 Vulnerable plugins report and WordPress in 2017

I do these updates and vulnerable plugin reports for the University of Missouri campus and thought I’d include them here as well.

Everyone should be updated to WordPress version 4.7 by now.  If not, please do so as soon as you can.  Lots of new, exciting features were added: WordPress 4.7 announcement and changelog.

If you didn’t follow Matt Mullenweg’s State of the Word this year from WordCamp US, you can watch it online (jump to 1:22:27  to see me question Matt on WordPress security issues).  If you’re interested, I also wrote up my key take-aways from WordCamp: Part 1 and Part 2

One of the big announcements from Matt was that he is taking back over as product lead for 2017 and that there will be no scheduled releases for WordPress in 2017.  Instead, the core team will be focusing on a simpler, faster UX (specifically the post editor) and more power for developers.  Minor point releases for bugs and security issues will be released as necessary, but large point releases will not be on a schedule. 

One of the big announcements for v4.7 was the core team added multiple content endpoints for the new REST API.  Unfortunately, one of those endpoints is users.   This means that anyone can remotely query your site for a list of your users.  Despite all of our efforts to lock down this sensitive information leakage, WordPress has added yet another way to retrieve this information.  To disable this “feature”, add the code from this gist into your functions.php file in your theme.  

You also might have heard quite a bit recently about the remote code execution vulnerability inside of PHPMailer which is included in WordPress core. While it is a critical vulnerability, several pieces have to align correctly in order for it to be exploited inside of WordPress.  An attacker would either need to combine multiple successful attacks, or already have an admin account on the site.  And if they have an admin account already, you’re already in trouble.  I mention it because WordPress will be updating their version in the coming days so make sure to update as soon as it is released.  More importantly, I would begin looking through your theme and plugins to see if they have included the vulnerable version.  If so, I would suggest manually updating the PHPMailer version, or discontinue use of the theme/plugin until that file has been updated.

Last, but not least, the vulnerable plugins report for 20170104:

https://docs.google.com/spreadsheets/d/1It-bOSM3AR_PVjKINvCe0bhiePEgT6L1EC4Sq3UxBIQ/edit?usp=sharing