logo-unlock-security

CVE-2023-5098

Image link

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

Autore

Francesco Marano

Prodotto affetto

Riferimenti

Descrizione del prodotto

Campaign Monitor Forms è un plugin per WordPress che permette di creare dei form di sottoscrizione al sito con funzionalità di monitoraggio delle performance e del numero di conversioni.

Dettagli della vulnerabilità

Quando si fa code review nei plugin WordPress si tende a cercare subito SQL injection, XSS o RCE, ma si tende a non dare peso ad altri tipi di vulnerabilità specifici della piattaforma. Un esempio di questo è l'uso non controllato di input utente nella funzione update_option di cui riportiamo la firma a seguire:

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

Lo scopo di questa funzione è quello di aggiornare il valore di un'opzione di WordPress, o di aggiungerne una nuova con il valore specificato. Quando parliamo di opzioni in WordPress ci si riferisce alla tabella wp-options del database in cui sono contenute tutte le opzioni sia di WordPress che di tutti i plugin installati che ne prevedono l'uso.

Quando però si utilizza l'input utente per scegliere quale opzione andare ad aggiornare si sta lasciando agli attaccanti una via per prendere il controllo dell'intero sito web. Tra le opzioni, infatti, sono presenti siteurl che modificato a un URL arbitrario crea un redirect permanente del sito, users_can_register che stabilisce se il form di registrazione è abilitato o meno, o ancora default_role che stabilisce il ruolo di default dei nuovi utenti.

Campaign Monitor Forms è un plugin che permette di prendere il controllo dell'opzione da modificare, ma l'impatto in questo caso è limitato; vediamo perché analizzando un estratto del codice vulnerabile:

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(); }

Come si può vedere viene registrata una Action AJAX autenticata che quando richiamata invoca la funzione ajax_dismiss_notice. Questa funzione verifica che il nonce sia corretto dopodiché utilizza il valore del parametro option passato in GET o in POST per aggiornare la corrispondente opzione sul database al valore true. Poter controllare il nome dell'opzione, ma non il suo valore limita molto le opzioni che possono essere modificate con successo. Questo perché andando ad analizzare l'implementazione della funzione update_option si nota subito del fatto che internamente viene invocata la funzione sanitize_option che controlla il formato del valore specificato rispetto all'opzione che si sta modificando, ad esempio:

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; // … }

Come triggherare il codice vulnerabile? È sufficiente creare una richiesta GET o POST verso /wp-admin/wp-ajax.php specificando i parametri action=fca_eoi_dismiss e option=QUALSIASI_OPZIONE, ma abbiamo bisogno anche di un nonce. Dato che lo scopo della funzione sembra essere quello di disabilitare un qualche tipo di notifica, allora sicuramente il codice che crea la notifica (quindi il nonce) controllerà che non sia già stata disabilitata. Cerchiamo quindi il nome dell'opzione che stabilisce se la notifica è stata disabilitata oppure no, cioè: fca_eoi_dismiss_review.

Abbiamo un match poche righe sopra la funzione vulnerabile:

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>'; } } }

Quindi, per poter ottenere un nonce valido abbiamo bisogno di almeno 25 conversioni (utenti che si sono iscritti tramite il form realizzato con il plugin) e che la notifica non sia già stata disabilitata. Rispettando queste due condizioni otteniamo la notifica, quindi il nonce:

Campaign Monitor Forms - notifica di recensione con codice nonce

Proof of Concept

A questo punto abbiamo tutto il necessario per sfruttare la vulnerabilità:

Campaign Monitor Forms - Richiesta e risposta HTTP per sfruttare la vulnerabilità
Campaign Monitor Forms - Tabella wp-options modificata a fronte della richiesta malevola

Disclosure timeline

01/09/2023

Vulnerabilità segnalata a WPScan

20/09/2023

Assegnata CVE-2023-5098

27/09/2023

Rilasciata fix ufficiale

09/10/2023

Advisory pubblicato da WPScan

30/10/2023

Pubblicata Proof of Concept (PoC)