logo-unlock-security

CVE-2023-5098

Image link

Campaign Monitor Forms by Optin Cat < 2.5.6 - Arbitrary Options Update using true value only

Author

Francesco Marano

Affected product

Product description

Campaign Monitor Forms is a WordPress plugin that allows you to create site subscription forms with performance and conversion tracking capabilities.

Vulnerability details

When doing code reviews in WordPress plugins, people tend to look for SQL injection, XSS, or RCE right away, but they tend to overlook other types of platform-specific vulnerabilities. An example of this is the unsanitized use of user input in the function update_option whose signature is shown below:

update_option( string $option, mixed $value, string|bool $autoload = null ): bool

The purpose of this function is to update the value of a WordPress option, or to add a new one with the specified value. When we talk about options in WordPress we are referring to the wp-options table in the database where all the options of both WordPress and all installed plugins which use them are contained.

However, when you use user input to choose which option to update you are giving to attackers a way to potentially take control of the entire Web site. Options, in fact, include siteurl which modified to an arbitrary URL creates a permanent website redirect, users_can_register which determines whether the registration form is enabled or not, or even default_role which determines the default role of new users.

Campaign Monitor Forms is a plugin that allows you to take control of the option being edited, but the impact in this case is limited; let's see why by analyzing an excerpt of the vulnerable code:

includes/eoi-post-types.php
// … add_action( 'wp_ajax_fca_eoi_dismiss', array( $this, 'ajax_dismiss_notice' ) ); // … public function ajax_dismiss_notice() { $nonce = empty ( $_REQUEST['nonce'] ) ? '' : $_REQUEST['nonce']; $option = empty ( $_REQUEST['option'] ) ? 'fca_eoi_dismiss_review' : $_REQUEST['option']; if ( wp_verify_nonce ( $nonce, 'fca_eoi_dismiss') == 1 ) { if ( update_option( $option, 'true' ) ) { wp_send_json_success( $option ); } } wp_send_json_error(); }

As you can see an authenticated AJAX Action is registered which when called invokes the function ajax_dismiss_notice. This function checks that the nonce is correct and after that it uses the value of the parameter option passed in GET or POST to update the corresponding option on the database to the value true. Being able to control the name of the option but not its value significantly limits the options that can be successfully changed. This is clear by analyzing the implementation of function update_option function where it is easy to notice that the sanitize_option function is invoked, which checks the format of the specified value against the option being modified, for example:

wp-includes/formatting.php
/** * Sanitizes various option values based on the nature of the option. * * This is basically a switch statement which will pass $value through a number * of functions depending on the $option. * * @since 2.0.5 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $option The name of the option. * @param string $value The unsanitized value. * @return string Sanitized value. */ function sanitize_option( $option, $value ) { global $wpdb; $original_value = $value; $error = null; switch ( $option ) { // … case 'users_can_register': // … $value = absint( $value ); break; // … case 'siteurl': $value = $wpdb->strip_invalid_text_for_column( $wpdb->options, 'option_value', $value ); if ( is_wp_error( $value ) ) { $error = $value->get_error_message(); } else { if ( preg_match( '#http(s?)://(.+)#i', $value ) ) { $value = sanitize_url( $value ); } else { $error = __( 'The WordPress address you entered did not appear to be a valid URL. Please enter a valid URL.' ); } } break; // … }

How to trigger vulnerable code? You only need to create a GET or POST request to /wp-admin/wp-ajax.php specifying the parameters action=fca_eoi_dismiss and option=QUALSIASI_OPZIONE, but we also need a nonce. Since the purpose of the function seems to be to disable some type of notification, then surely the code that creates the notification (thus the nonce) will check that it has not already been disabled. We then look for the name of the option that determines whether the notification has been disabled or not, namely: fca_eoi_dismiss_review.

We have a match a few lines above the vulnerable function:

includes/eoi-post-types.php
public function review_notice() { $dismissed = get_option ( 'fca_eoi_dismiss_review' ); if ( $dismissed !== 'true' ) { $activity = EasyOptInsActivity::get_instance(); $stats = $activity->get_form_stats( $this->activity_day_interval['form_list'] ); $conversions = empty ( $stats['conversions'] ) ? array() : $stats['conversions']; $conversions = array_sum ( $conversions ); if ( $conversions >= 25 ) { $review_link = 'https://wordpress.org/support/plugin/' . FCA_EOI_PLUGIN_SLUG . '/reviews/?rate=5#new-post'; wp_enqueue_script( 'fca_eoi_dismiss_review_js', FCA_EOI_PLUGIN_URL . '/assets/admin/dismiss.min.js', array(), FCA_EOI_VER ); wp_localize_script( 'fca_eoi_dismiss_review_js', 'fcaEoiDismiss', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'fca_eoi_dismiss' ) ) ); echo '<div class="notice notice-success fca_eoi_review_div">'; echo '<img style="float:left" width="120" height="120" src="' . FCA_EOI_PLUGIN_URL . '/assets/admin/optincat.png' . '">'; echo '<p><strong>' . __( "Great work! You've gotten more than 25 email subscribers using Optin Cat.", 'easy-opt-ins' ) . '</strong></p>'; echo '<p>' . sprintf( __( "If you love Optin Cat, why not leave us a nice review on %sWordPress.org%s? Reviews keeps us motivated - we'd really appreciate it.", 'easy-opt-ins' ), "<a target='_blank' href='$review_link'>", '</a>') . '</p>'; echo '<br>'; echo "<a target='_blank' href='$review_link' class='button button-primary'>" . __( 'Leave a Review', 'easy-opt-ins') . "</a> "; echo "<button type='button' class='button button-secondary' data-option='fca_eoi_dismiss_review' id='fca-eoi-dismiss-review-btn'>" . __( 'Dismiss', 'easy-opt-ins') . "</button>"; echo '<br style="clear:both">'; echo '</div>'; } } }

Thus, in order to get a valid nonce we need at least 25 conversions (users who signed up via the form made with the plugin) and that the notification has not already been disabled. By fulfilling these two conditions we get the notification, thus the nonce:

Campaign Monitor Forms - review notification with nonce code

Proof of Concept

At this point we have everything we need to exploit the vulnerability:

Campaign Monitor Forms - HTTP request and response to exploit vulnerability
Campaign Monitor Forms - Modified wp-options table by malicious request

Disclosure timeline

01/09/2023

Vulnerability submitted to WPScan

20/09/2023

Assigned CVE-2023-5098

27/09/2023

Official fix released

09/10/2023

Advisory published by WPScan

30/10/2023

Proof of Concept (PoC) published