13 Jul 2016

Protecting You Against Wordfence’s Bad Practices: XSS Vulnerability in All in One SEO Pack

Wordfence is putting WordPress website at risk by disclosing vulnerabilities in plugins with critical details needed to double check their work missing, in what appears to be an attempt to profit off of these vulnerabilities. We are releasing those details so that others can review the vulnerabilities to try to limit the damage Wordfence’s practice could cause.

The latest in our ongoing series of putting out the details of details of vulnerabilities discovered by Wordfence is good example of why what Wordfence is doing is hurting the security of WordPress plugins. In this case they saw a report of  a persistent cross-site scripting (XSS) vulnerability in the plugin All in One SEO Pack and discovered a similar vulnerability, which is something that often happens we security researchers see reports of vulnerabilities in plugins. The difference is that with that report, like other reports by responsible parties, it included the details of the vulnerabilities, so it was easy for Wordfence to see what the issue was in that case. By Wordfence excluding those details it makes it harder to do the same with vulnerabilities that they have discovered, but through our work on this we have already found two additional security vulnerabilities in the Yoast SEO plugin and one in the WP Fastest Cache plugin.

Wordfence describes this vulnerability as “unauthenticated stored XSS vulnerability allows an attacker to inject javascript code into a page that requires admin privileges to view. When a site admin visits the page, the malicious code that runs can perform administrative actions such as modifying existing user privileges, creating a new admin user or stealing admin session tokens.” and “This exploit only works if the user has enabled the sitemap module in the plugin.”

Looking at the changes made in the version that fixed this, 2.3.8, the first part that stands out is escaping is now done on a debug message for the sitemap module.

In version 2.3.7 the code looked like this (in the file /modules/aioseop_sitemap.php):

if ( $this->option_isset( 'debug' ) ) {
 $options["{$this->prefix}debug"] = '<pre>' . $options["{$this->prefix}debug"] . '</pre>';
}

In version 2.3.8 the value is run through esc_html():

if ( $this->option_isset( 'debug' ) ) {
 $debug_msg = esc_html( $options["{$this->prefix}debug"] );
 $options["{$this->prefix}debug"] = '<pre>' . $debug_msg . '</pre>';
}

So the persistent cross-site scripting (XSS) was occurring in the debug messages, but we still need to find how it could be set by an unauthenticated attacker.

That brings us to other change made in that version, which was to modify the function log_stats().

In version 2.3.7 it looked like this (in the file /modules/aioseop_sitemap.php):

1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
function log_stats( $sitemap_type = 'root', $compressed = false, $dynamic = true ) {
	$time                 = timer_stop();
	$end_memory_usage     = memory_get_peak_usage();
	$sitemap_memory_usage = $end_memory_usage - $this->start_memory_usage;
	$end_memory_usage     = $end_memory_usage / 1024.0 / 1024.0;
	$sitemap_memory_usage = $sitemap_memory_usage / 1024.0 / 1024.0;
	if ( $compressed ) {
		$sitemap_type = __( 'compressed', 'all-in-one-seo-pack' ) . " $sitemap_type";
	}
	if ( $dynamic ) {
		$sitemap_type = __( 'dynamic', 'all-in-one-seo-pack ' ) . " $sitemap_type";
	} else {
		$sitemap_type = __( 'static', 'all-in-one-seo-pack ' ) . " $sitemap_type";
	}
	$this->debug_message( sprintf( ' %01.2f MB memory used generating the %s sitemap in %01.3f seconds, %01.2f MB total memory used.', $sitemap_memory_usage, $sitemap_type, $time, $end_memory_usage ) );
}

In version 2.3.8 it looks like this:

1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
function log_stats( $sitemap_type = 'static', $compressed = false, $dynamic = true ) {
	$time                 = timer_stop();
	$end_memory_usage     = memory_get_peak_usage();
	$sitemap_memory_usage = $end_memory_usage - $this->start_memory_usage;
	$end_memory_usage     = $end_memory_usage / 1024.0 / 1024.0;
	$sitemap_memory_usage = $sitemap_memory_usage / 1024.0 / 1024.0;
	$sitemap_type         = __( 'static', 'all-in-one-seo-pack ' );
	if ( $compressed ) {
		$sitemap_type = __( 'compressed', 'all-in-one-seo-pack' );
	}
	if ( $dynamic ) {
		$sitemap_type = __( 'dynamic', 'all-in-one-seo-pack ' );
	}
	$this>debug_message( sprintf( ' %01.2f MB memory used generating the %s sitemap in %01.3f seconds, %01.2f MB total memory used.', $sitemap_memory_usage, $sitemap_type, $time, $end_memory_usage ) );
}

The key difference being that the value of $sitemap_type, which used in the debug message, is set in the function instead having the possibility of being the value passed to the function.

A little looking over the rest of the code shows why that is important. The function sitemap_output_hook() is made accessible when not logged in to WordPress through the this line:

1041
add_action( 'parse_query', array( $this, 'sitemap_output_hook' ) );

The function sitemap_output_hook() will call the function log_stats() that we just looked at with a user specified value for $sitemap_type set in this line:

1166
$sitemap_type             = $query->query_vars["{$this->prefix}path"];

Since there is no sanitization done at that point, you could have previously sent a request with malicious JavaScript code set to that URL parameter and it would have been shown in debug message on the sitemap module’s page in the admin.

Proof of Concept

The following proof of concept will cause any available cookies to be shown in alert box on the page /wp-admin/admin.php?page=all-in-one-seo-pack%2Fmodules%2Faioseop_sitemap.php.

Make sure to replace “[path to WordPress]” with the location of WordPress.

http://[path to WordPress]/?aiosp_sitemap_path=<script>alert(document.cookie);</script>

Leave a Reply

Your email address will not be published.