Campaign Monitor Forms by Optin Cat < 2.5.6 - Arbitrary Options Update using true value only
Autore
Francesco Marano
Prodotto affetto
Campaign Monitor Forms by Optin Cat < 2.5.6 (WordPress plugin)
Categoria CWE
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.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>'; } } }
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:
Proof of Concept
A questo punto abbiamo tutto il necessario per sfruttare la vulnerabilità:
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)