Remote SQL Injection in WordPress and WordPress MU

Author
Alexander Concha <alex at buayacorp dot com>
Affected versions
WordPress <= 2.2.2 and WordPress MU <= 1.2.4

Overview

WordPress is a state-of-the-art semantic personal publishing platform with a focus on aesthetics, web standards, and usability.

While testing WordPress, it has been discovered a SQL Injection vulnerability that allows an attacker to retrieve remotely any user credentials from a vulnerable site, this bug is caused because of early database escaping and the lack of validation in query string like parameters.

Introduction

Many WordPress functions accept and are called using query string like parameters, that kind of functions use internally parse_str to parse parameters:

function wp_parse_args( $args, $defaults = '' ) {
	if ( is_object($args) )
		$r = get_object_vars($args);
	else if ( is_array( $args ) )
		$r =& $args;
	else
		wp_parse_str( $args, $r );

	if ( is_array( $defaults ) )
		return array_merge( $defaults, $r );
	else
		return $r;
}
function wp_parse_str( $string, &$array ) {
	parse_str( $string, $array );
	if ( get_magic_quotes_gpc() )
		$array = stripslashes_deep( $array );
	$array = apply_filters( 'wp_parse_str', $array );
}

Some code to illustrate the problem:

// This code does not exists on WP codebase
function get_posts($args = '') {
	$defaults = array(
		'post_type' => 'post',
		'max_results' => 5
	);

	$r = wp_parse_args( $args, $defaults );
	extract( $r, EXTR_SKIP );
	
	echo "
		SELECT 	* 
		FROM 	wp_posts 
		WHERE 	post_type = '$post_type' 
		LIMIT 	$max_results
	";
}
...

// Somewhere in the application

$args = 'post_type=' . $wpdb->escape($_GET['post_type']);
$posts = get_posts($args);

// Some results with different values for post_type

$_GET['post_type'] = 'page'
--------------------------------
SELECT 	* 
FROM 	wp_posts 
WHERE 	post_type = 'page' 
LIMIT 	5
	
$_GET['post_type'] = 'demo%27 SQL INJECTION HERE /*'
--------------------------------
SELECT 	* 
FROM 	wp_posts 
WHERE 	post_type = 'demo' SQL INJECTION HERE /*' 
LIMIT 	5
	
$_GET['post_type'] = 'dummy&max_results=0 SQL INJECTION'
--------------------------------
SELECT 	* 
FROM 	wp_posts 
WHERE 	post_type = 'dummy' 
LIMIT 	0 SQL INJECTION

Because of early database escaping and the lack of validation in this query string parameters -- as shown by the example, WordPress is vulnerable to multiple SQL Injection vulnerabilities, some of them are remotely exploitable.

Details

WordPress provides and XMLRPC interface to list pingbacks for an specific post, it uses url_to_postid function to determine the post ID of an URL.

Relevant part of url_to_postid function:

foreach ($rewrite as $match => $query) {
	// If the requesting file is the anchor of the match, prepend it
	// to the path info.
	if ( (! empty($url)) && (strpos($match, $url) === 0) ) {
		$request_match = $url . '/' . $request;
	}

	if ( preg_match("!^$match!", $request_match, $matches) ) {
		// Got a match.
		// Trim the query of everything up to the '?'.
		$query = preg_replace("!^.+\?!", '', $query);

		// Substitute the substring matches into the query.
		eval("\$query = \"$query\";");
		$query = new WP_Query($query);
		if ( $query->is_single || $query->is_page )
			return $query->post->ID;
		else
			return 0;
	}
}
...
An example of $match [->] $query
category_base/([^/]+)(/[0-9]+)?/?$ [->] name=$matches[1]&page=$matches[2]

An attacker can prepare a crafted URL to pass arbitrary parameters to get_posts method of WP_Query class.

http://vulnerable.com/category_base/&post_type=%27) SQL INJECTION HERE %2F*

If a vulnerable blog has permalinks enabled and the above URL matches some rewrite rule, it will override the value of $post_type variable.

Proof of Concept

An exploit code was developed to test a vulnerable version in 2.2 series, it uses XMLRPC API.

Solution

Upgrade as soon as posible to the latest version of WordPress [MU] or block the access to xmlrpc.php.

Disclosure Timeline