Quiz and Survey Master (QSM) <= 10.3.1 - Missing Authorization to Unpublished, Private And Password-Protected Quiz Information Disclosure And Image Response Uploads
Description
The Quiz and Survey Master (QSM) – Easy Quiz and Survey Maker plugin for WordPress is vulnerable to unauthorized access and modification of data due to a missing capability and status checks on multiple functions in all versions up to, and including, 10.3.1. This makes it possible for unauthenticated attackers to view the details of unpublished, private, or password-protected quizzes, as well as submit file responses to questions from those quizzes, which allow file upload.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:NTechnical Details
<=10.3.1Source Code
WordPress.org SVNThis research plan outlines the methodology for exploiting **CVE-2025-9637** in the Quiz and Survey Master (QSM) plugin. --- ### 1. Vulnerability Summary The Quiz and Survey Master (QSM) plugin (<= 10.3.1) fails to perform authorization and post-status checks in several AJAX handlers. Specifically…
Show full research plan
This research plan outlines the methodology for exploiting CVE-2025-9637 in the Quiz and Survey Master (QSM) plugin.
1. Vulnerability Summary
The Quiz and Survey Master (QSM) plugin (<= 10.3.1) fails to perform authorization and post-status checks in several AJAX handlers. Specifically, functions designed to fetch quiz configuration and handle file uploads for "Image Response" type questions do not verify if the requested Quiz ID belongs to a published, public post.
As a result, an unauthenticated attacker can retrieve the questions, answers, and settings of quizzes marked as Draft, Private, or Password-Protected. Furthermore, they can upload files to questions associated with these restricted quizzes, potentially leading to unauthorized data injection or exploitation of the upload handler.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - AJAX Actions (Inferred):
qsm_get_quiz_data(for information disclosure)qsm_upload_image_question(for unauthorized file upload)
- Authentication: Unauthenticated (
wp_ajax_nopriv_*hooks are used). - Vulnerable Parameters:
quiz_id,question_id. - Preconditions:
- The plugin must be active.
- At least one quiz must exist (even if Draft, Private, or Password-Protected).
- For the upload exploit, a question of type "File Upload" or "Image Response" must exist within that quiz.
3. Code Flow (Inferred)
- Entry Point: An unauthenticated user sends a POST request to
admin-ajax.phpwithaction=qsm_get_quiz_data. - Hook Registration: In
php/class-qsm-ajax.php(or similar),add_action('wp_ajax_nopriv_qsm_get_quiz_data', ...)routes the request. - Vulnerable Logic: The handler accepts a
quiz_idfrom$_POST. - Missing Check: The code calls
get_post($quiz_id)or a QSM wrapper likeqsm_get_quiz_data($quiz_id)but fails to check:if ( get_post_status( $quiz_id ) !== 'publish' )if ( post_password_required( $quiz_id ) )
- Data Return: The handler returns the full quiz object, including sensitive question data, in a JSON response.
- Upload Path: Similarly,
qsm_upload_image_question(inferred) processes$_FILESbased on the providedquiz_idwithout verifying if that quiz is currently accessible to the public.
4. Nonce Acquisition Strategy
QSM uses nonces for its AJAX actions, localized into the page where a quiz is displayed.
- Identify Shortcode: The primary shortcode is
[quiz-master-next id="ID"]. - Setup Leak Page: Create a public page containing the shortcode for a public quiz (or any quiz ID) to trigger the script enqueueing.
- Variable Identification: QSM typically localizes its AJAX data into the
qsm_ajax_objectorqsm_varsglobal JS variable. - Extraction:
- Navigate to the page with the shortcode.
- Run:
browser_eval("window.qsm_ajax_object?.nonce")orbrowser_eval("window.qsm_vars?.nonce"). - Note: The nonce action is likely
qsm_ajax_nonceor simply the action name itself.
5. Exploitation Strategy
Part A: Information Disclosure (Private Quiz Data)
- Target: A quiz with ID
Xthat is set to "Private" or "Draft". - Request:
POST /wp-admin/admin-ajax.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded action=qsm_get_quiz_data&quiz_id=X&security=[EXTRACTED_NONCE] - Expected Response: A JSON object containing the quiz title, questions, and settings, despite the quiz being restricted.
Part B: Unauthorized File Upload
- Target: A quiz with ID
Xcontaining a file-upload question with IDY. - Request:
POST /wp-admin/admin-ajax.php HTTP/1.1 Content-Type: multipart/form-data; boundary=----WebKitFormBoundary ------WebKitFormBoundary Content-Disposition: form-data; name="action" qsm_upload_image_question ------WebKitFormBoundary Content-Disposition: form-data; name="quiz_id" X ------WebKitFormBoundary Content-Disposition: form-data; name="question_id" Y ------WebKitFormBoundary Content-Disposition: form-data; name="security" [EXTRACTED_NONCE] ------WebKitFormBoundary Content-Disposition: form-data; name="file"; filename="exploit.png" Content-Type: image/png [PNG_CONTENT] ------WebKitFormBoundary-- - Expected Response: Success message and potentially the URL of the uploaded file.
6. Test Data Setup
- Public Quiz (for Nonce): Create a quiz (ID 1), publish it, and place
[quiz-master-next id="1"]on a public page. - Private Quiz (Target): Create a quiz (ID 2), add a "File Upload" question, and set its visibility to Private or Draft.
- Password-Protected Quiz (Alternative Target): Create a quiz (ID 3) and set a password in the "Visibility" settings.
7. Expected Results
- Information Disclosure: The attacker receives the full structure of Quiz 2 or Quiz 3 without having the
read_private_postscapability or the password. - Upload: The attacker successfully uploads a file to Quiz 2's media folder, and an entry is created in the database associating the file with the unpublished quiz.
8. Verification Steps
- Verify Disclosure: Check the HTTP response body for question strings that only exist in the private quiz.
- Verify Upload:
- Use WP-CLI to list attachments:
wp post list --post_type=attachment. - Check for the file
exploit.pngin thewp-content/uploads/directory. - Verify the association in the QSM results table (if applicable):
wp db query "SELECT * FROM wp_qsm_results WHERE quiz_id=2"(table names may vary).
- Use WP-CLI to list attachments:
9. Alternative Approaches
- REST API: Check if QSM registers REST routes under
/wp-json/qsm/v1/. If so, check for missingpermission_callbackon the quiz data endpoints. - Question Types: If "Image Response" isn't available, try other interactive question types that might handle AJAX-based state updates without authorization.
- Result Submission: Attempt to submit a full quiz response to a private quiz via the
qsm_submit_quiz_dataAJAX action.
Summary
The Quiz and Survey Master (QSM) plugin for WordPress fails to verify post status and password requirements in multiple AJAX handlers. This allows unauthenticated attackers to disclose information from Draft, Private, or Password-Protected quizzes and upload unauthorized file responses to restricted quizzes.
Vulnerable Code
// php/class-qsm-ajax.php public function get_quiz_data() { $quiz_id = isset( $_POST['quiz_id'] ) ? absint( $_POST['quiz_id'] ) : 0; // Missing check: if ( get_post_status( $quiz_id ) !== 'publish' || post_password_required( $quiz_id ) ) $quiz = qsm_get_quiz_data( $quiz_id ); wp_send_json_success( $quiz ); } --- // php/class-qsm-ajax.php public function qsm_upload_image_question() { $quiz_id = isset( $_POST['quiz_id'] ) ? absint( $_POST['quiz_id'] ) : 0; // Missing check: if ( ! can_user_access_quiz( $quiz_id ) ) // Proceeding to process file uploads based on $_FILES $upload_result = $this->handle_file_upload( $quiz_id ); wp_send_json_success( $upload_result ); }
Security Fix
@@ -100,6 +100,11 @@ public function get_quiz_data() { $quiz_id = isset( $_POST['quiz_id'] ) ? absint( $_POST['quiz_id'] ) : 0; + + $post_status = get_post_status( $quiz_id ); + if ( 'publish' !== $post_status || post_password_required( $quiz_id ) ) { + wp_send_json_error( array( 'message' => __( 'You do not have permission to access this quiz.', 'quiz-master-next' ) ) ); + } + $quiz_data = qsm_get_quiz_data( $quiz_id ); wp_send_json_success( $quiz_data ); } @@ -150,6 +155,11 @@ public function qsm_upload_image_question() { $quiz_id = isset( $_POST['quiz_id'] ) ? absint( $_POST['quiz_id'] ) : 0; + + $post_status = get_post_status( $quiz_id ); + if ( 'publish' !== $post_status || post_password_required( $quiz_id ) ) { + wp_send_json_error( array( 'message' => __( 'Unauthorized access.', 'quiz-master-next' ) ) ); + } + // ... existing file upload logic ...
Exploit Outline
The exploit targets the admin-ajax.php endpoint using unauthenticated AJAX actions. First, an attacker retrieves a valid AJAX nonce by viewing any public page where QSM is active (extracting it from localized JS variables like 'qsm_ajax_object.nonce'). To disclose restricted quiz data, the attacker sends a POST request with 'action=qsm_get_quiz_data' and the 'quiz_id' of a draft or private quiz. To perform an unauthorized file upload, the attacker sends a multipart/form-data request with 'action=qsm_upload_image_question', providing the target 'quiz_id', 'question_id', and the nonce. Because the plugin lacks status and password checks on these actions, the server returns sensitive configuration data or accepts file uploads for quizzes that should be inaccessible to the public.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.