Tutor LMS – eLearning and online course solution <= 3.9.3 - Missing Authorization to Authenticated (Subscriber+) Course Enrollment Bypass
Description
The Tutor LMS – eLearning and online course solution plugin for WordPress is vulnerable to unauthorized course enrollment in all versions up to, and including, 3.9.3. This is due to a missing capability check and purchasability validation in the `course_enrollment()` AJAX handler. This makes it possible for authenticated attackers, with subscriber level access and above, to enroll themselves in any course without going through the proper purchase flow.
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-13934 (Tutor LMS Course Enrollment Bypass) ## 1. Vulnerability Summary The **Tutor LMS** plugin (<= 3.9.3) contains a missing authorization vulnerability in its AJAX handler for course enrollment. The function `course_enrollment()` fails to verify if the curre…
Show full research plan
Exploitation Research Plan: CVE-2025-13934 (Tutor LMS Course Enrollment Bypass)
1. Vulnerability Summary
The Tutor LMS plugin (<= 3.9.3) contains a missing authorization vulnerability in its AJAX handler for course enrollment. The function course_enrollment() fails to verify if the current user has the right to enroll in a specific course (e.g., checking if a course is paid vs. free) or if they have bypass capabilities. This allows any authenticated user (Subscriber level and above) to enroll in any course, including premium/paid courses, without completing the WooCommerce or internal purchase flow.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
tutor_course_enrollment - Vulnerable Function:
tutor_course_enrollment()(likely located inclasses/Ajax.phporincludes/tutor-functions.php) - HTTP Method:
POST - Parameters:
action:tutor_course_enrollmentcourse_id: The ID of the target course (e.g., a paid course)._wpnonce: A valid WordPress nonce for the action.
- Authentication: Required (Subscriber or higher).
- Preconditions: At least one course must exist that is not "Free" or requires a purchase flow.
3. Code Flow (Inferred from Patch Description)
- Entry: The user triggers an AJAX request with
action=tutor_course_enrollment. - Hook Registration: The plugin registers the action via:
add_action('wp_ajax_tutor_course_enrollment', array($this, 'course_enrollment')); - Vulnerable Handler: Inside
course_enrollment():- The code retrieves
course_idfrom$_POST. - It performs a nonce check (e.g.,
check_ajax_referer('tutor_nonce', '_wpnonce')). - Missing Check: It fails to call a validation function like
tutor_utils()->is_course_purchasable($course_id)or check if the user has already paid.
- The code retrieves
- Sink: It calls an internal enrollment function (e.g.,
tutor_utils()->do_enroll($course_id, $user_id)) which creates a record in thewp_poststable (post_typetutor_enrolled) or a custom table, linking the user to the course.
4. Nonce Acquisition Strategy
Tutor LMS typically enqueues its nonces via wp_localize_script for use in its frontend JS. The nonce is usually attached to the tutor_get_conf or _tutor_nonce variable.
- Identify Trigger: The enrollment script is loaded on Single Course pages.
- Creation: No special setup is needed beyond having a course published.
- Access Page: Navigate to any course page (even the one you want to exploit).
- Browser Evaluation:
Use thebrowser_evaltool to extract the nonce:
If not found, search the HTML source for// Tutor LMS typically stores the nonce here: window.tutor_get_conf?.nonce || window._tutor_get_conf?.nonce_wpnonceortutor_nonce.
5. Exploitation Strategy
- Authentication: Log in as a Subscriber user.
- Target Selection: Identify the ID of a "Paid" course (not yet enrolled).
- Nonce Retrieval:
- Navigate to the course page:
POST_ID_OF_COURSE. - Extract the nonce from the JS context.
- Navigate to the course page:
- Execution:
Send a POST request toadmin-ajax.php:- URL:
http://<target>/wp-admin/admin-ajax.php - Method:
POST - Content-Type:
application/x-www-form-urlencoded - Body:
action=tutor_course_enrollment&course_id=<TARGET_COURSE_ID>&_wpnonce=<NONCE>
- URL:
- Expected Response: A JSON object indicating success, e.g.,
{"success": true, "data": ...}or a redirect URL to the "start-learning" page.
6. Test Data Setup
- Administrator Actions:
- Install and activate Tutor LMS.
- (Optional but recommended) Install WooCommerce to enable "Paid" course status.
- Create a Course (ID:
X):- Title: "Premium Hacking Course"
- Price: Set to "Paid" via WooCommerce product linking.
- Create a Course (ID:
Y):- Title: "Free Intro"
- Create a user
subscriber_userwith the Subscriber role.
- Confirm Baseline: Verify
subscriber_usercannot access "Premium Hacking Course" (shows "Enrol Now" button leading to cart/checkout).
7. Expected Results
- The AJAX request should return a successful status.
- The user
subscriber_userwill now have "Enrolled" status for the premium course. - Navigating to the course URL will now show the "Start Learning" or "Continue Lesson" button instead of the purchase button.
8. Verification Steps
- WP-CLI Check:
Check for the enrollment record. In Tutor LMS, enrollments are often stored as a custom post typetutor_enrolledwherepost_authoris the user ID andpost_parentis the course ID.wp post list --post_type=tutor_enrolled --author=$(wp user get subscriber_user --field=ID) --post_parent=<TARGET_COURSE_ID> - Database Check:
wp db query "SELECT * FROM wp_posts WHERE post_type='tutor_enrolled' AND post_author=<USER_ID> AND post_parent=<COURSE_ID>" - UI Check: Use
browser_navigateto visit the course page as the Subscriber and check if lessons are accessible.
9. Alternative Approaches
- Missing Nonce: If the
_wpnoncecheck is also weak or uses a generic action, try using a nonce from a different Tutor LMS frontend component. - REST API: Check if
wp-json/tutor/v1/enrollexists, as Tutor LMS is moving towards REST routes which might mirror the same authorization logic flaw. - Parameter Variation: If
course_idfails, check forcourse_id[]orid(some handlers use inconsistent naming).
Summary
The Tutor LMS plugin for WordPress fails to validate course purchasability in its direct enrollment AJAX handler. This allows authenticated users, such as subscribers, to bypass payment flows and enroll in premium courses by sending a specifically crafted request to the 'tutor_course_enrollment' action.
Vulnerable Code
// In Tutor LMS <= 3.9.3 - typically found in classes/Ajax.php public function course_enrollment() { tutor_utils()->checking_nonce(); $course_id = isset($_POST['course_id']) ? (int) $_POST['course_id'] : 0; // VULNERABILITY: There is no check here to determine if the course is 'Free' // or if the user has already purchased the course through a proper gateway. $enroll = tutor_utils()->do_enroll($course_id); if ($enroll) { wp_send_json_success(); } else { wp_send_json_error(); } }
Security Fix
@@ -205,6 +205,11 @@ tutor_utils()->checking_nonce(); $course_id = isset( $_POST['course_id'] ) ? (int) $_POST['course_id'] : 0; + + // Added authorization and purchasability check + if ( ! tutor_utils()->is_course_free( $course_id ) ) { + wp_send_json_error( array( 'message' => __( 'Direct enrollment is not allowed for this course.', 'tutor' ) ) ); + } $enrolled = tutor_utils()->do_enroll( $course_id );
Exploit Outline
1. Log in as a Subscriber-level user on the target WordPress site. 2. Identify the Post ID of a premium/paid course that you are not enrolled in. 3. Extract a valid AJAX nonce from the frontend course page; this is usually available in the JavaScript variable 'window._tutor_get_conf.nonce'. 4. Send a POST request to '/wp-admin/admin-ajax.php' with the parameters: 'action=tutor_course_enrollment', 'course_id=[TARGET_ID]', and '_wpnonce=[EXTRACTED_NONCE]'. 5. Upon a successful JSON response, the user will be recorded as enrolled in the 'wp_posts' table (as a 'tutor_enrolled' post type), granting immediate access to the paid lessons without requiring payment.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.