AppSec Blog

Spot the Vuln - Assassins - Cross Site Scripting

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 &rsaquo; 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






Captcha


* Indicates a required field.