
Campaign Monitor Forms by Optin Cat < 2.5.6 - Arbitrary Options Update using true value only
Author
Francesco Marano
Affected product
Campaign Monitor Forms by Optin Cat < 2.5.6 (WordPress plugin)
CWE category
References
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.phppublic 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:
Proof of Concept
At this point we have everything we need to exploit the vulnerability:
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