Details
Affected Software: WordPress Core
Fixed in Version: 2.2-alpha
Issue Type: Cross Site Scripting
Original Code: Found Here
Details
A couple of bugs affecting WordPress core here. On line 73, we see that $_SERVER['REQUEST_URI'] is passed to add_query_arg(). From the provided code sample, it's difficult to see that this results in XSS. The developers addressed this by encoding the return value from add_query_arg().
The second issue starts at wp_get_referer(). This function checks $_REQUEST['_wp_http_referer'] and/or $_SERVER['HTTP_REFERER'] for possible values. This makes is so the attacker has several options for tainting wp_get_referer(). Passing a tainted GET or POST parameter will do the trick. Simply redirecting to the vulnerable WordPress installation from a tainted URL will also taint the value returned from wp_get_referer().
Later, in wp_nonce_ays(), the tainted wp_get_referer() value is passed to the $adminurl variable. $adminurl is then used to build HTML markup in a couple of different places. The WP developers addressed this issue by passing wp_get_referer() before assinging that value to $adminurl. Seems like an effective fix?
Well, url values are always a bit tricky. These values have to be encoded before being used to build HTML markup. URLs also have to be validated if they are being passed to a SRC, HREF, or other HTML attribute which causes a browser navigation or request. In this example, the tainted $adminurl is used to populate an A HREF value. The value is encoded, however that encoding doesn't stop an attacker from passing a javascript:payload url...
Developers Solution
<?php
...snip...
function wp_original_referer_field() {
echo '<input type="hidden" name="_wp_original_http_referer" value="' . attribute_escape(stripslashes($_SERVER['REQUEST_URI'])) . '" />';
}
function wp_get_referer() {
foreach ( array($_REQUEST['_wp_http_referer'], $_SERVER['HTTP_REFERER']) as $ref )
if ( !empty($ref) )
return $ref;
return false;
}
function wp_get_original_referer() {
if ( !empty($_REQUEST['_wp_original_http_referer']) )
return $_REQUEST['_wp_original_http_referer'];
return false;
}
function wp_mkdir_p($target) {
// from php.net/mkdir user contributed notes
if (file_exists($target)) {
if (! @ is_dir($target))
return false;
else
return true;
}
// Attempting to create the directory may clutter up our display.
if (@ mkdir($target)) {
$stat = @ stat(dirname($target));
$dir_perms = $stat['mode'] & 0007777; // Get the permission bits.
@ chmod($target, $dir_perms);
return true;
} else {
if ( is_dir(dirname($target)) )
return false;
}
// If the above failed, attempt to create the parent node, then try again.
if (wp_mkdir_p(dirname($target)))
return wp_mkdir_p($target);
return false;
}
...snip...
function wp_nonce_ays($action) {
global $pagenow, $menu, $submenu, $parent_file, $submenu_file;
$adminurl = get_option('siteurl') . '/wp-admin';
if ( wp_get_referer() )
-$adminurl = wp_get_referer();
+$adminurl = attribute_escape(wp_get_referer());
$title = __('WordPress Confirmation');
// Remove extra layer of slashes.
$_POST = stripslashes_deep($_POST );
if ( $_POST ) {
$q = http_build_query($_POST);
$q = explode( ini_get('arg_separator.output'), $q);
$html .= "\t<form method='post' action='$pagenow'>\n";
foreach ( (array) $q as $a ) {
$v = substr(strstr($a, '='), 1);
$k = substr($a, 0, -(strlen($v)+1));
$html .= "\t\t<input type='hidden' name='" . attribute_escape(urldecode($k)) . "' value='" . attribute_escape(urldecode($v)) . "' />\n";
}
$html .= "\t\t<input type='hidden' name='_wpnonce' value='" . wp_create_nonce($action) . "' />\n";
$html .= "\t\t<div id='message' class='confirm fade'>\n\t\t<p>" . wp_specialchars(wp_explain_nonce($action)) . "</p>\n\t\t<p><a href='$adminurl'>" . __('No') . "</a> <input type='submit' value='" . __('Yes') . "' /></p>\n\t\t</div>\n\t</form>\n";
} else {
-$html .= "\t<div id='message' class='confirm fade'>\n\t<p>" . wp_specialchars(wp_explain_nonce($action)) . "</p>\n\t<p><a href='$adminurl'>" . __('No') . "</a> <a href='" . add_query_arg( '_wpnonce', wp_create_nonce($action), $_SERVER['REQUEST_URI'] ) . "'>" . __('Yes') . "</a></p>\n\t</div>\n";
+ $html .= "\t<div id='message' class='confirm fade'>\n\t<p>" . wp_specialchars(wp_explain_nonce($action)) . "</p>\n\t<p><a href='$adminurl'>" . __('No') . "</a> <a href='" . attribute_escape(add_query_arg( '_wpnonce', wp_create_nonce($action), $_SERVER['REQUEST_URI'] )) . "'>" . __('Yes') . "</a></p>\n\t</div>\n";
}
$html .= "</body>\n</html>";
wp_die($html, $title);
}
function wp_die($message, $title = ") {
global $wp_locale;
header('Content-Type: text/html; charset=utf-8');
if ( empty($title) )
$title = __('WordPress › Error');
if ( strstr($_SERVER['PHP_SELF'], 'wp-admin') )
$admin_dir = ";
else
$admin_dir = 'wp-admin/';
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" <?php if ( function_exists('language_attributes') ) language_attributes(); ?>>
<head>
<title><?php echo $title ?></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="stylesheet" href="<?php echo $admin_dir; ?>install.css" type="text/css" />
<?php if ( ('rtl' == $wp_locale->text_direction) ) : ?>
<link rel="stylesheet" href="<?php echo $admin_dir; ?>install-rtl.css" type="text/css" />
<?php endif; ?>
</head>
<body>
<h1 id="logo"><img alt="WordPress" src="<?php echo $admin_dir; ?>images/wordpress-logo.png" /></h1>
<p><?php echo $message; ?></p>
</body>
</html>
...snip...
Post a Comment
* Indicates a required field.