endangcamon/CVE-2026-7465-POC
GitHub: endangcamon/CVE-2026-7465-POC
Stars: 2 | Forks: 0
# CVE-2026-7465 - Spectra Gutenberg Blocks <= 2.19.25
# Authenticated (Contributor+) Remote Code Execution via Arbitrary PHP Function Call via Block Attributes
================================================================================
## VULNERABILITY SUMMARY
================================================================================
| Field | Value |
|------------------------|----------------------------------------------------------|
| CVE ID | CVE-2026-7465 |
| Plugin | Spectra Gutenberg Blocks - Website Builder for Block Editor |
| Vulnerable Versions | <= 2.19.25 |
| Attack Type | Remote Code Execution (RCE) |
| Access Level Required | Contributor and above |
| Attack Complexity | Low |
| User Interaction | None (exploit triggers on post view) |
| CVSS Score | Critical (estimated 9.9) |
| Vulnerable File | classes/class-uagb-init-blocks.php (lines 329-336) |
================================================================================
## VULNERABILITY DETAILS
================================================================================
### Root Cause
The `render_block()` method in `class-uagb-init-blocks.php` passes
user-controlled block attributes (`$block['attrs']`) directly to
`WP_Block_Type_Registry::register()` without ANY validation or
sanitization of the `render_callback` field.
**Vulnerable Code (lines 329-336):**
// Register only UAG blocks.
if ( ! empty( $block['blockName'] ) && strpos( $block['blockName'], 'uagb/' ) !== false ) {
// Register block on server-side to support WP Hide blocks feature introduce in WP 6.9.
$registry = WP_Block_Type_Registry::get_instance();
// Only register if the block is NOT already registered.
if ( ! $registry->is_registered( $block['blockName'] ) ) {
$registry->register( $block['blockName'], $block['attrs'] ); // <-- VULNERABLE LINE
}
}
### Why This Is Dangerous
`$block['attrs']` comes directly from the post content (block comment
delimiters). Any user who can create or edit posts (Contributor+) can
embed arbitrary JSON attributes in block comments. When these attributes
are passed to `WP_Block_Type_Registry::register()`, they become
properties of the new `WP_Block_Type` object, including:
- `render_callback` - Arbitrary PHP callable (function name, class method)
- `attributes` - Block attribute schema
- `script_handles` / `view_script_handles` - Script handles to enqueue
- `style_handles` - Style handles to enqueue
The most critical property is `render_callback`. Setting this to any
valid PHP callable makes the block "dynamic" (`is_dynamic()` returns
true when `is_callable($render_callback)` is true). When WordPress
encounters a registered dynamic block, it calls:
call_user_func( $this->block_type->render_callback, $this->attributes, $block_content, $this );
This invokes the attacker-specified function with attacker-controlled
arguments, achieving Arbitrary PHP Function Call.
================================================================================
## EXPLOITATION TECHNIQUE
================================================================================
### Two-Block Payload
The exploit requires exactly two blocks in the same post content:
**BLOCK 1 (Registration Block):**
- Block name: Any `uagb/`-prefixed name (e.g., `uagb/cve-2026-7465`)
- Block attributes: `{"render_callback": ""}`
- Purpose: When Spectra's `render_block` filter processes this block,
it registers the fake block type with the attacker-specified
`render_callback` in the `WP_Block_Type_Registry`.
**BLOCK 2 (Trigger Block):**
- Block name: SAME as Block 1 (e.g., `uagb/cve-2026-7465`)
- Block attributes: Function-specific arguments
- Inner content: Optional, used as second argument to the callback
- Purpose: Since the block type is now registered with a `render_callback`,
WordPress's block rendering engine will call `call_user_func()` on it,
executing the attacker-specified function with the second block's
attributes and content as arguments.
### Sequential Block Rendering Flow
WordPress parses post content → encounters Block 1 (uagb/cve-2026-7465)
│
├─ Is block registered? → NO → Output static HTML (no render_callback called)
│
├─ apply_filters('render_block') → Spectra's render_block() at priority 5
│ ├─ Block name starts with "uagb/"? → YES
│ ├─ Is registered? → NO
│ └─ $registry->register("uagb/cve-2026-7465", {"render_callback": "wp_insert_user"})
│ → WP_Block_Type created with malicious render_callback
│
WordPress continues → encounters Block 2 (uagb/cve-2026-7465)
│
├─ Is block registered? → YES (registered by Block 1)
│
├─ is_dynamic()? → YES (is_callable("wp_insert_user") = true)
│
├─ call_user_func("wp_insert_user", $attributes, $content, $block)
│ → wp_insert_user(array with admin user data) → ADMIN USER CREATED!
│
└─ apply_filters('render_block') → Spectra's render_block() at priority 5
├─ Block name starts with "uagb/"? → YES
└─ Is registered? → YES → Skip (already registered)
================================================================================
## EXPLOIT VARIANTS
================================================================================
### Variant 1: Privilege Escalation via wp_insert_user (PHP 8.x Safe)
**Impact:** Administrator user creation → Full site takeover → RCE
Block 1:
Block 2:
**Why it works on PHP 8.x:**
- `wp_insert_user` is a WordPress userland function (defined in PHP)
- PHP userland functions silently ignore extra arguments (deprecation notice only)
- The 3-argument call `call_user_func("wp_insert_user", $attrs, $content, $block)`
effectively becomes `wp_insert_user($attrs)` (extra args ignored)
- `$attrs` contains the full user creation array with role=administrator
**Post-exploitation:**
1. Log in as the new administrator
2. Navigate to Appearance > Theme File Editor
3. Edit a PHP template file with ``
4. Access the modified file URL for direct OS command execution
### Variant 2: Direct OS Command Execution via array_walk (PHP 7.x)
**Impact:** Immediate OS command execution
Block 1:
Block 2:
system
**Mechanism:**
- `array_walk($attributes, $content, $block)` is called by WordPress
- `array_walk` accepts `(array, callable, mixed)` → type-compatible in PHP 8.x
- For each element, calls `$content($value, $key, $userdata)`
- `system("whoami", 0, $block_obj)` → command executes (PHP 7.x)
- On PHP 8.x: `system()` is an internal function, ArgumentCountError
for the 3rd argument from `array_walk`
**Note:** This variant works reliably on PHP 7.x. On PHP 8.x, use
Variant 1 (wp_insert_user) instead.
### Variant 3: Arbitrary Post Creation via wp_insert_post
**Impact:** Content injection, phishing page creation, SEO spam
Block 1:
Block 2:
### Variant 4: Webshell Deployment Chain
**Impact:** Persistent PHP webshell on the server
This requires a multi-step chain:
1. Use `wp_insert_user` to create an admin user
2. Use the admin session to upload a malicious plugin via `wp-admin/plugin-install.php`
3. The plugin contains a PHP webshell
Alternatively, use `wp_insert_post` to create a post, then use admin
access to modify theme files via the theme editor.
================================================================================
## PROOF OF CONCEPT RESULTS (Lab Verified)
================================================================================
### Environment
- WordPress: Latest (6.x)
- PHP: 8.2.12 (XAMPP, Windows)
- Plugin: Spectra Gutenberg Blocks 2.19.25 (active)
- Database: MySQL (XAMPP)
### Test Results
**TEST 1: Arbitrary render_callback Registration**
- Registered fake block `uagb/poc-test-*` with `render_callback = "phpinfo"`
- Result: SUCCESS - Block type registered, `is_dynamic()` = true
**TEST 2: wp_insert_user Privilege Escalation (PHP 8.2)**
- Registered block with `render_callback = "wp_insert_user"`
- Called `call_user_func("wp_insert_user", $user_data, "", null)`
- Result: SUCCESS - Administrator user created (ID: 19)
- Verification: User role confirmed as `administrator` in database
**TEST 3: array_walk + passthru Direct RCE (PHP 8.2)**
- Registered block with `render_callback = "array_walk"`
- Called `array_walk(array("whoami"), "passthru", $mock_block)`
- Result: FAILED - PHP 8.2 ArgumentCountError (passthru expects max 2 args)
- Note: Would work on PHP 7.x where extra args produce warnings only
**TEST 4: wp_insert_post Content Injection (PHP 8.2)**
- Registered block with `render_callback = "wp_insert_post"`
- Called `call_user_func("wp_insert_post", $post_data, "", null)`
- Result: SUCCESS - New post created (ID: 82)
**TEST 5: Full HTTP Exploit Chain (Frontend Rendering)**
- Created post (ID: 83) with two-block payload via direct DB
- Accessed post via HTTP: `GET /WordpressLabReseacher/?p=83`
- Spectra's `render_block` filter registered the fake block type
- WordPress rendered second block → `wp_insert_user` called
- Result: SUCCESS - Admin user `rce_poc_admin_0752d0ac` (ID: 21) created
- Role verified: `a:1:{s:13:"administrator";b:1;}`
================================================================================
## REMEDIATION
================================================================================
### Recommended Fix
The `render_block()` method should validate `$block['attrs']` before
passing it to `WP_Block_Type_Registry::register()`. Specifically:
1. **Whitelist allowed properties**: Only pass known-safe block type
properties (e.g., `attributes` schema, `supports`). Never pass
`render_callback` from user-controlled data.
2. **Strip dangerous properties**: Remove `render_callback`,
`script_handles`, `view_script_handles`, and any other properties
that could be abused before registering the block type.
3. **Validate block names**: Only register block names that correspond
to actual Spectra block types (check against a known list of valid
block slugs).
### Example Fix
public function render_block( $block_content, $block ) {
if ( ! empty( $block['blockName'] ) && strpos( $block['blockName'], 'uagb/' ) !== false ) {
$registry = WP_Block_Type_Registry::get_instance();
if ( ! $registry->is_registered( $block['blockName'] ) ) {
// FIXED: Remove dangerous properties from attrs before registering
$safe_attrs = $block['attrs'];
unset( $safe_attrs['render_callback'] );
unset( $safe_attrs['script_handles'] );
unset( $safe_attrs['view_script_handles'] );
unset( $safe_attrs['editor_script_handles'] );
unset( $safe_attrs['style_handles'] );
unset( $safe_attrs['view_style_handles'] );
unset( $safe_attrs['editor_style_handles'] );
unset( $safe_attrs['variation_callback'] );
$registry->register( $block['blockName'], $safe_attrs );
}
}
// ... rest of method
}
================================================================================
## TIMELINE
================================================================================
- Vulnerability introduced: When the render_block() registration
feature was added (to support WP Hide blocks in WP 6.9)
- Affected versions: All versions up to and including 2.19.25
- Patched version: > 2.19.25 (pending/available)
================================================================================
## REFERENCES
================================================================================
- https://plugins.trac.wordpress.org/browser/ultimate-addons-for-gutenberg/trunk/classes/class-uagb-init-blocks.php#L335
- https://plugins.trac.wordpress.org/browser/ultimate-addons-for-gutenberg/tags/2.19.25/classes/class-uagb-init-blocks.php#L335
- https://plugins.trac.wordpress.org/browser/ultimate-addons-for-gutenberg/trunk/classes/class-uagb-init-blocks.php#L330
- https://plugins.trac.wordpress.org/browser/ultimate-addons-for-gutenberg/tags/2.19.25/classes/class-uagb-init-blocks.php#L330
================================================================================
## FILES IN THIS DIRECTORY
================================================================================
================================================================================