WP-CRM System – Manage Clients and Projects <= 3.4.5 - Missing Authorization to Authenticated (Subscriber+) CRM Data Exposure and Task Modification
Description
The WP-CRM System plugin for WordPress is vulnerable to unauthorized access due to missing capability checks on the wpcrm_get_email_recipients and wpcrm_system_ajax_task_change_status AJAX functions in all versions up to, and including, 3.4.5. This makes it possible for authenticated attackers, with subscriber level access and above, to enumerate CRM contact email addresses (PII disclosure) and modify CRM task statuses. CVE-2025-62106 is likely a duplicate of this issue.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:NTechnical Details
<=3.4.5What Changed in the Fix
Changes introduced in v3.4.6
Source Code
WordPress.org SVN# Exploitation Research Plan: WP-CRM System <= 3.4.5 Missing Authorization ## 1. Vulnerability Summary The **WP-CRM System** plugin for WordPress (versions <= 3.4.5) contains a missing authorization vulnerability within its AJAX handlers. Specifically, functions responsible for retrieving CRM task …
Show full research plan
Exploitation Research Plan: WP-CRM System <= 3.4.5 Missing Authorization
1. Vulnerability Summary
The WP-CRM System plugin for WordPress (versions <= 3.4.5) contains a missing authorization vulnerability within its AJAX handlers. Specifically, functions responsible for retrieving CRM task details (wp_crm_system_ajax_task_list) and updating task statuses (wpcrm_system_ajax_task_change_status) are registered via wp_ajax_ but fail to implement capability checks (e.g., current_user_can()).
This allows any authenticated user, including those with Subscriber level permissions, to:
- Enumerate CRM Data: View details of any task by ID, including associated contact names, organization names, and project names.
- Modify Task Statuses: Change the status of any CRM task.
- PII Disclosure (Inferred): The description also mentions
wpcrm_get_email_recipients(code not provided) allowing enumeration of contact email addresses.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Actions:
task_change_status(Triggerswpcrm_system_ajax_task_change_status)task_list_response(Triggerswp_crm_system_ajax_task_list)wpcrm_get_email_recipients(Triggers inferred PII disclosure)
- Authentication: Authenticated, Subscriber level or higher (
wp_ajax_hooks). - Preconditions: A valid CRM task ID must exist in the database for the exploit to show impact.
3. Code Flow
Task Modification (task_change_status)
- Entry Point:
add_action( 'wp_ajax_task_change_status', 'wpcrm_system_ajax_task_change_status' );inincludes/wcs-dashboard-task-list.php. - Input: Takes
post_idandtask_statusfrom$_POST. - Processing:
wpcrm_system_ajax_task_change_status()directly callsupdate_post_meta( $post_id, '_wpcrm_task-status', $task_status ). - Vulnerability: There is no nonce verification and no capability check. Any logged-in user can invoke this.
CRM Data Disclosure (task_list_response)
- Entry Point:
add_action( 'wp_ajax_task_list_response', 'wp_crm_system_ajax_task_list' );inincludes/wcs-dashboard-task-list.php. - Input: Takes
task_idandtask_list_noncefrom$_POST. - Processing:
wp_crm_system_ajax_task_list()verifies the nonce, then retrieves task metadata:_wpcrm_task-attach-to-organization_wpcrm_task-attach-to-contact_wpcrm_task-attach-to-project
- Output: Returns an HTML table containing titles of the organization, contact, and project.
- Vulnerability: While a nonce check exists, there is no capability check. Any user who can obtain the
task-list-nonce(localized on admin pages) can view any task's internal CRM associations.
4. Nonce Acquisition Strategy
The task_change_status action does not require a nonce.
The task_list_response action requires task_list_nonce.
Strategy to obtain task_list_nonce:
- Localization: The nonce is localized in
wpcrm_system_dashboard_task_jsviawp_localize_script. - Target Hook: It is hooked to
admin_enqueue_scripts, meaning it is likely present on any admin page for an authenticated user. - Extraction:
- Login as a Subscriber.
- Navigate to
/wp-admin/profile.php. - Use
browser_evalto extract:window.task_list_vars?.task_list_nonce.
5. Exploitation Strategy
The agent will perform two primary exploits: unauthorized task status modification and unauthorized data disclosure.
Step 1: Unauthorized Task Status Modification
- Method: POST
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Body (URL-encoded):
action=task_change_status &post_id=[TASK_ID] &task_status=completed - Expected Response:
success
Step 2: Unauthorized Data Disclosure
- Method: POST
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Body (URL-encoded):
action=task_list_response &task_list_nonce=[EXTRACTED_NONCE] &task_id=[TASK_ID] - Expected Response: An HTML table containing titles of CRM objects (Contacts/Projects) that the Subscriber should not be able to see.
6. Test Data Setup
To demonstrate impact, the environment must contain CRM data:
- Create a CRM Contact:
wp post create --post_type=wpcrm-contact --post_title="Secret Client" --post_status=publish - Create a CRM Project:
wp post create --post_type=wpcrm-project --post_title="Confidential Project" --post_status=publish - Create a CRM Task and link it:
- Create task:
TASK_ID=$(wp post create --post_type=wpcrm-task --post_title="Initial Task" --post_status=publish --porcelain) - Attach contact:
wp post meta add $TASK_ID _wpcrm_task-attach-to-contact [CONTACT_ID] - Set initial status:
wp post meta add $TASK_ID _wpcrm_task-status "not-started"
- Create task:
- Create Subscriber User:
wp user create attacker attacker@example.com --role=subscriber --user_pass=password
7. Expected Results
- Modification: The
task_change_statusrequest returns "success". Verification via WP-CLI shows the post meta_wpcrm_task-statuschanged fromnot-startedtocompleted. - Disclosure: The
task_list_responsereturns HTML containing the string "Secret Client" and "Confidential Project", proving the Subscriber accessed restricted CRM relationships.
8. Verification Steps
After performing the HTTP requests:
- Check Status Change:
wp post meta get [TASK_ID] _wpcrm_task-status
(Expected:completed) - Verify Access to Contact Name: Compare the HTTP response from
task_list_responseagainst the string "Secret Client".
9. Alternative Approaches
If task_list_nonce is not present on profile.php, the agent should check if the plugin registers a specific dashboard page for tasks.
Search for add_menu_page or add_submenu_page in the code:grep -r "add_submenu_page" includes/
If a "Tasks" page exists that a Subscriber can access, the nonce will certainly be there.
If wpcrm_get_email_recipients is the target:
- Identify the action via
grep -r "wp_ajax_wpcrm_get_email_recipients" . - Send a POST request to
admin-ajax.phpwithaction=wpcrm_get_email_recipients. - Observe if it returns a list of email addresses from the
wpcrm-contactposts.
Summary
The WP-CRM System plugin for WordPress contains missing authorization and nonce checks in its AJAX and GDPR-related handlers. This allows authenticated users with subscriber-level permissions to modify CRM task statuses, enumerate CRM task details (including linked organizations and contacts), and potentially access or delete PII through improperly secured GDPR export/deletion features.
Vulnerable Code
// includes/wcs-dashboard-task-list.php (approx. line 173) function wpcrm_system_ajax_task_change_status() { $post_id = isset( $_POST['post_id'] ) ? sanitize_text_field( $_POST['post_id'] ) : ''; $task_status = isset( $_POST['task_status'] ) ? sanitize_text_field( $_POST['task_status'] ) : ''; $meta = 'status'; $update_status = update_post_meta( $post_id, '_wpcrm_task-' . $meta, $task_status ); if ( $update_status ) { echo 'success'; } else { echo 'fail'; } exit; } --- // includes/wcs-dashboard-task-list.php (approx. line 7) function wp_crm_system_ajax_task_list() { if ( ! isset( $_POST['task_list_nonce'] ) || ! wp_verify_nonce( $_POST['task_list_nonce'], 'task-list-nonce' ) ) { die( 'Permissions check failed' ); } if ( ! isset( $_POST['task_id'] ) ) { die( 'No task data sent' ); } // ... (retrieves and displays CRM task meta like organization, contact, and project names) --- // includes/gdpr-export-contact.php (approx. line 28) public function get_cpt_post_ids(){ $ids = array( $_GET['contact_id'] ); return $ids; }
Security Fix
@@ -26,7 +26,32 @@ } public function get_cpt_post_ids(){ - $ids = array( $_GET['contact_id'] ); + // Security: Validate and sanitize contact_id + if ( ! isset( $_GET['contact_id'] ) || empty( $_GET['contact_id'] ) ) { + wp_die( esc_html__( 'Invalid request. Contact ID is required.', 'wp-crm-system' ) ); + } + + $contact_id = absint( $_GET['contact_id'] ); + + // Security: Verify contact exists and is correct post type + if ( 0 === $contact_id || 'wpcrm-contact' !== get_post_type( $contact_id ) ) { + wp_die( esc_html__( 'Invalid contact ID.', 'wp-crm-system' ) ); + } + + // Security: Validate GDPR secret token to prevent unauthorized access + if ( ! isset( $_GET['secret'] ) || empty( $_GET['secret'] ) ) { + wp_die( esc_html__( 'Invalid request. Secret token is required.', 'wp-crm-system' ) ); + } + + $secret = sanitize_text_field( $_GET['secret'] ); + $contact_secret = get_post_meta( $contact_id, '_wpcrm_system_gdpr_secret', true ); + + // Security: Verify secret token matches the contact's stored secret + if ( empty( $contact_secret ) || $secret !== $contact_secret ) { + wp_die( esc_html__( 'Invalid request. Secret token does not match.', 'wp-crm-system' ) ); + } + + $ids = array( $contact_id ); return $ids; }
Exploit Outline
1. Authentication: Log in to the WordPress site as a user with Subscriber-level permissions. 2. Nonce Retrieval (for Data Disclosure): Access any admin page where the plugin enqueues its dashboard scripts (e.g., `/wp-admin/profile.php`). Extract the `task_list_nonce` from the `task_list_vars` JavaScript variable in the page source. 3. Task Modification: Send a POST request to `/wp-admin/admin-ajax.php` with the action `task_change_status`, a valid CRM task `post_id`, and a new `task_status`. The server will update the task status without a capability check or nonce verification. 4. Data Enumeration: Send a POST request to `/wp-admin/admin-ajax.php` with the action `task_list_response`, the retrieved `task_list_nonce`, and a valid `task_id`. The server returns an HTML table disclosing the titles and edit links for linked Organizations, Contacts, and Projects. 5. Unauthorized GDPR Access: If contact IDs are known, an attacker can attempt to access the GDPR export functionality by navigating to the GDPR page shortcode URL without a valid 'secret' parameter, as earlier versions lacked server-side verification of the secret token against stored post meta.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.