Tutor LMS <= 3.9.7 - Missing Authorization
Description
The Tutor LMS plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in versions up to, and including, 3.9.7. This makes it possible for authenticated attackers, with subscriber-level access and above, to perform an unauthorized action.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:NTechnical Details
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-40740 - Tutor LMS Missing Authorization ## 1. Vulnerability Summary The **Tutor LMS** plugin (<= 3.9.7) contains a missing authorization vulnerability within its AJAX handling logic. Specifically, several functions registered via `wp_ajax_` hooks fail to imple…
Show full research plan
Exploitation Research Plan: CVE-2026-40740 - Tutor LMS Missing Authorization
1. Vulnerability Summary
The Tutor LMS plugin (<= 3.9.7) contains a missing authorization vulnerability within its AJAX handling logic. Specifically, several functions registered via wp_ajax_ hooks fail to implement current_user_can() checks, relying solely on nonce verification. Since the tutor_nonce is often exposed to all authenticated users (including Subscribers), an attacker can perform administrative-level actions such as modifying or viewing quiz configurations, adding questions, or altering course settings.
In version 3.9.7, the logic resides primarily in classes/Ajax.php, where handlers for quiz management (e.g., tutor_load_edit_quiz_modal) were accessible to any logged-in user who could obtain a valid nonce.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
tutor_load_edit_quiz_modal(and potentially others liketutor_add_quiz_question) - Vulnerable Parameter:
quiz_id - Required Authentication: Subscriber-level (authenticated)
- Preconditions:
- The attacker must be logged in as a Subscriber.
- A quiz must exist in the system (the attacker needs a valid
quiz_id). - The attacker must obtain the
tutor_nonce.
3. Code Flow
- Entry Point: The plugin registers AJAX actions in
classes/Ajax.php.// tutor/classes/Ajax.php (inferred) add_action( 'wp_ajax_tutor_load_edit_quiz_modal', array( $this, 'tutor_load_edit_quiz_modal' ) ); - Nonce Verification: The handler calls
check_ajax_refererto ensure the request is "valid".public function tutor_load_edit_quiz_modal() { check_ajax_referer( 'tutor_nonce', 'tutor_nonce' ); // Nonce check only // ... missing current_user_can() check ... $quiz_id = (int) $_POST['quiz_id']; // Logic to fetch and return quiz editor HTML } - Missing Auth: The code proceeds to fetch quiz data based on the
quiz_idprovided in$_POSTwithout checking if the current user is the author of the course or an administrator. - Sink: The plugin returns the HTML/JSON for the quiz editor, exposing internal settings and question data.
4. Nonce Acquisition Strategy
Tutor LMS localizes the tutor_nonce for its frontend and backend scripts. For a Subscriber, the easiest way to obtain the nonce is to visit the Tutor Dashboard page.
- Shortcode Page: Create or identify a page containing the
[tutor_dashboard]shortcode. - Navigation: Navigate to this page as the Subscriber user.
- Extraction: The nonce is stored in the global JavaScript object
tutor_get_conf.- Variable:
window.tutor_get_conf - Key:
nonce
- Variable:
- Agent Command:
browser_eval("window.tutor_get_conf?.nonce")
5. Exploitation Strategy
The goal is to use a Subscriber account to access the quiz editor modal for a quiz the user does not own.
Step-by-step:
- Login: Authenticate as a Subscriber.
- Nonce Retrieval: Navigate to the page with
[tutor_dashboard]and extract the nonce usingbrowser_eval. - Target Identification: Identify a
quiz_id(a Post ID for thetutor_quizzespost type). - Trigger Request: Send a POST request to
admin-ajax.php.
HTTP Request:
POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
action=tutor_load_edit_quiz_modal&tutor_nonce=[EXTRACTED_NONCE]&quiz_id=[TARGET_QUIZ_ID]
Alternative Attack (Modification):
If tutor_add_quiz_question is also vulnerable (likely), you can inject questions into a quiz:
POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
action=tutor_add_quiz_question&tutor_nonce=[EXTRACTED_NONCE]&quiz_id=[TARGET_QUIZ_ID]&question_title=Hacked+Question&question_type=single_choice
6. Test Data Setup
- Admin Actions:
- Create a Course titled "Secret Course".
- Add a Quiz titled "Final Exam" to the course.
- Note the ID of the Quiz (e.g.,
123). - Create a page with the shortcode
[tutor_dashboard]at/tutor-dashboard/.
- Attacker Actions:
- Create a user with the
subscriberrole.
- Create a user with the
7. Expected Results
- Success: The HTTP response for
tutor_load_edit_quiz_modalreturns a status of 200 and a JSON/HTML payload containing the quiz settings (e.g.,quiz_title, time limits, and existing questions). - Failure: The response returns a 403 Forbidden or an error message indicating insufficient permissions (the patch behavior).
8. Verification Steps
- Check Output: Inspect the response body of the AJAX request for strings like
"tutor-quiz-builder-modal"or the specific title of the targeted quiz. - Verify Modification (if applicable): Use WP-CLI to verify if a question was added to the quiz:
wp post list --post_type=tutor_quiz_question --post_parent=[QUIZ_ID] - Verify Auth Requirement: Attempt the same request without the Subscriber cookies; it should fail at the nonce check.
9. Alternative Approaches
If tutor_load_edit_quiz_modal is strictly read-only, look for these other AJAX actions registered in the same classes/Ajax.php file:
tutor_save_quiz_sortable_data(Allows reordering questions)tutor_quiz_builder_get_question_form(Exposes individual question details)hide_tutor_notice(Low impact, but demonstrates missing authorization by dismissing admin notices)
Note on Nonce Variable Names: If tutor_get_conf is not found, check for tutor_ajax_main or tutor_localize_data. These are common fallbacks in different Tutor LMS versions.
Summary
Tutor LMS (<= 3.9.7) is vulnerable to unauthorized access because several AJAX handlers, such as 'tutor_load_edit_quiz_modal', lack proper capability checks. Authenticated users, including those with subscriber-level access, can obtain a valid nonce from the dashboard and perform actions like viewing or modifying quiz configurations they do not own.
Vulnerable Code
// tutor/classes/Ajax.php public function tutor_load_edit_quiz_modal() { check_ajax_referer( 'tutor_nonce', 'tutor_nonce' ); $quiz_id = (int) $_POST['quiz_id']; // ... missing current_user_can() check ... // Logic returns quiz editor HTML containing sensitive settings and questions } --- // tutor/classes/Ajax.php public function tutor_add_quiz_question() { check_ajax_referer( 'tutor_nonce', 'tutor_nonce' ); // Missing authorization check $quiz_id = (int) $_POST['quiz_id']; // Logic proceeds to add questions to the quiz }
Security Fix
@@ -10,6 +10,10 @@ public function tutor_load_edit_quiz_modal() { check_ajax_referer( 'tutor_nonce', 'tutor_nonce' ); + if ( ! current_user_can( 'tutor_instructor' ) && ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied', 'tutor' ) ) ); + } + $quiz_id = (int) $_POST['quiz_id'];
Exploit Outline
1. Authenticate as a Subscriber-level user. 2. Navigate to a page containing the [tutor_dashboard] shortcode (e.g., /tutor-dashboard/) and extract the 'tutor_nonce' from the 'window.tutor_get_conf' JavaScript object. 3. Identify the target Quiz Post ID (quiz_id). 4. Submit a POST request to /wp-admin/admin-ajax.php with the action 'tutor_load_edit_quiz_modal', the extracted nonce, and the target quiz_id. 5. Observe the response containing the HTML/JSON for the quiz editor, exposing internal settings and question data without permission.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.