Attacker Value
Very Low
(1 user assessed)
Exploitability
Very High
(1 user assessed)
User Interaction
Unknown
Privileges Required
Unknown
Attack Vector
Unknown
1

CVE-2024-31077

Disclosure Date: April 23, 2024
Add MITRE ATT&CK tactics and techniques that apply to this CVE.

Description

Forminator prior to 1.29.3 contains a SQL injection vulnerability. If this vulnerability is exploited, a remote authenticated attacker with an administrative privilege may obtain and alter any information in the database and cause a denial-of-service (DoS) condition.

Add Assessment

3
Ratings
  • Attacker Value
    Very Low
  • Exploitability
    Very High
Technical Analysis

Forminator Wordpress plugin versions prior to 1.29.3 are vulnerable to SQL injection. After investigating the changes made in version 1.29.3, it is easy to see that no input sanitization was done on the order_by and order parameters before being concatenated to the SQL statement. This code lies in the get_filter_entries() function in library/model/class-form-entry-model.php:

		$order_by = 'ORDER BY entries.entry_id';
		if ( isset( $filters['order_by'] ) ) {
			$order_by = 'ORDER BY ' . esc_sql( $filters['order_by'] ); // unesacaped.
		}
		$order = 'DESC';
		if ( isset( $filters['order'] ) ) {
			$order = esc_sql( $filters['order'] );
		}

		// group.
		$group_by = 'GROUP BY entries.entry_id';

		$sql     = "SELECT entries.`entry_id` FROM {$table_name} entries
						INNER JOIN {$entries_meta_table_name} AS metas
    					ON (entries.entry_id = metas.entry_id)
 						WHERE {$where} {$group_by} {$order_by} {$order}";
		$results = $wpdb->get_results( $wpdb->prepare( $sql, $form_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

This function is called by _prepare_export_data() in library/class-export.php when exporting Quiz or Form data as a CSV or sent as an attachment via email. This code path is reached when filters like order or order_by are used in the query.

To demonstrate this, authenticate as an admin to access the Wordpress Admin Dashboard, go to /wp-admin/admin.php?page=forminator-entries, click on the filter button on the right hand side of the submission list, add some filters and click APPLY. The ones that interest us are Sort By and Sort Order. Then, click on EXPORT, select Apply Submission Filters in the Manual Exports section and click DOWNLOAD CSV.

This will send the following POST request:

POST /wp-admin/admin.php?page=forminator-entries&form_type=forminator_forms&form_id=7&entries-action&date_range&min_id&max_id&search&order_by=entries.date_created&order=DESC&entry_status=all&entries-action-bottom HTTP/1.1
Host: localhost:8080
Content-Length: 96
…[SNIP]...

forminator_export=1&form_id=7&form_type=cform&_forminator_nonce=ed27a59f8a&submission-filter=yes

It should return the submission entries in CSV format.

Now, let’s inject some SQL commands in the order_by parameter. According to the code, the vulnerable SQL query should not return anything in the response and we’ll need to go with blind SQLi. Since it will be concatenated to the ORDER BY clause, we will use the following query:

1,(select if((1=1),1,(select 1 union select 2)))

If the if condition is true, the submission entries should be returned. If it is false, an empty list should be returned:

  • True (1=1)
    True result
  • False (1=0)
    False result

More precisely, it is an Blind Error-Based SQLi, since a false statement will fail with an SQL error: ERROR 1242 (21000): Subquery returns more than 1 row

We now confirmed that SQLi is possible.

One big caveat to this attack is that each time a CSV is required, Forminator updates the forminator_exporter_log Wordpress option with a timestamp:

 124         $count = $export_data->entries_count;
 125         // save the time for later uses.
 126         $logs = get_option( 'forminator_exporter_log', array() );
 127         if ( ! isset( $logs[ $model->id ] ) ) {
 128             $logs[ $model->id ] = array();
 129         }
 130         $logs[ $model->id ][] = array(
 131             'time'  => current_time( 'timestamp' ),
 132             'count' => $count,
 133         );
 134         update_option( 'forminator_exporter_log', $logs );

This will cause Wordpress to update the forminator_exporter_log option value in the wp_options table each time a request is received. This code is not overwriting the previous timestamp but actually adding a new timestamp to the option each time a CSV is required. As a result, the forminator_exporter_log option value will increase in size each time it is updated.

This is not a big problem per se, but if binary logging is enabled (it is enabled by default from MySQL 8.0), each database update will add an event to the binary log file (binlog.*). Since a blind SQLi usually requires a lot of queries, the binary log files will increase exponentially and quickly fill out all the disk space on the MySQL server. This will end up causing a DoS (it happened to me many times while investigating).

To conclude, this vulnerability is relatively easy to exploit but requires privileged access to Wordpress in order to reach the Forminator CSV export functionality. Even with these privileges, it is very likely to cause a DoS before any useful data is retrieved from the database.

CVSS V3 Severity and Metrics
Base Score:
None
Impact Score:
Unknown
Exploitability Score:
Unknown
Vector:
Unknown
Attack Vector (AV):
Unknown
Attack Complexity (AC):
Unknown
Privileges Required (PR):
Unknown
User Interaction (UI):
Unknown
Scope (S):
Unknown
Confidentiality (C):
Unknown
Integrity (I):
Unknown
Availability (A):
Unknown

General Information

Vendors

  • WPMU DEV

Products

  • Forminator

Additional Info

Technical Analysis