Remote Code Execution – Cwicly <= 1.4.0.2

| in


Affected pluginCwicly
Active installsNot available – Commercial
Vulnerable version<= 1.4.0.2
Audited version1.4.0.2
Fully patched version1.4.0.3
Recommended remediationUpgrade immediately to version 1.4.0.3 or higher.

Description


The Cwicly page builder is vulnerable to remote code execution (RCE) in versions <= 1.4.0.2, which means that an attacker can run arbitrary code/system commands and take over the site/server.

To exploit this vulnerability, a user must have the edit_post(s) capability, which on stock WordPress installation means contributor and above.
Likewise, an attacker that steals the session cookie of a contributor can also exploit this.

On WordPress sites where users can create any sort of post type in the frontend, this vulnerability might be vulnerable without authentication, depending on the setup.

It’s advised that you update your sites immediately!

Technical Details


Cwicly has a code block feature that allows privileged users to write PHP snippets directly in the Gutenberg editor.

To execute these code blocks, Cwicly uses PHP’s eval functionality, which is notoriously dangerous.

This happens in “code.php” file, which registers a Gutenberg block using the standard WordPress Core APIs:

/**
 * Register block render callback.
 */
register_block_type(
	__DIR__,
	array(
		'render_callback' => 'cc_code_render_callback',
	)
);

/**
 * Render callback.
 *
 * @param array  $attributes Block attributes.
 * @param string $content Block content.
 * @param object $block Block data.
 *
 * @return string
 */
function cc_code_render_callback( $attributes, $content, $block ) {
   // 
}

The relevant part of cc_code_render_callback is this, where the (user) input from the $attributes variable is passed into eval.

Note that Cwicly prefixes the eval part with closing PHP tags (“?>”) to allow users to mix HTML and PHP in the code block. To get RCE, an attacker must pass a string starting with opening PHP tags (“<?php”). This will become important later.

if (isset($attributes['code'])) {
    ob_start();
    $eval = eval(' ?>'.$attributes['code'].'<?php ');
    $final = ob_get_clean();
}

This code becomes exploitable if we find a way to pass user controlled input into a Gutenberg block.

Naturally, the first choice for us to look at was creating a new post via the REST API that contains a maliciously crafted Cwicly code block.

Cwicly has a role editor and by default, nobody, not even admins, should be able to create code blocks.

Image 5

This is enforced by hooking into the wp_insert_post_data hook that is called when a post is created or updated. The hook can be used to return modified post data before it’s saved in the database.

Cwicly uses this mechanism to filter out any potential code blocks that might be present in the post.

This is done via the filter_saved_content callback:

/**
 * Checks the saved content and filter if necessary.
 *
 * @param array $data The data to check.
 * @param array $postarr The post array.
 * @param array $unsanitized_postarr The unsanitized post array.
 */
public static function filter_saved_content( $data, $postarr, $unsanitized_postarr )
{
    if ( ! current_user_can('manage_options')) {
        $patterns = ['{return=', '{site_option', '{siteoption'];
        $data['post_content'] = str_replace($patterns, '', $data['post_content']);
    }
    
    $id = $postarr['ID'];
    $previous_content = get_post_field('post_content', $id);
    
    if ($previous_content) {
        $previous_blocks = parse_blocks($previous_content);
        $current_blocks = parse_blocks(stripslashes($data['post_content']));
        
        $modified = false;
        self::check_replacement($previous_blocks, $current_blocks, 'cwicly/svg', $modified);
        self::check_replacement($previous_blocks, $current_blocks, 'cwicly/code', $modified);
        
        if ($modified) {
            $data['post_content'] = wp_slash(serialize_blocks($current_blocks));
        }
    } else {
        $modified = false;
        self::check_replacement([], $current_blocks, 'cwicly/svg', $modified);
        self::check_replacement([], $current_blocks, 'cwicly/code', $modified);
        
        if ($modified) {
            $data['post_content'] = wp_slash(serialize_blocks($current_blocks));
        }
    }
    
    return $data;
}

This method has two major code branches.

Line 16-28, checks if the currently saved post already exists, and line 30-37 is used for new posts.

Both code branches call the check_replacement() function which does the actual sanitization based on permissions.

But there’s a crucial bug in the branch for new posts. The $current_blocks variable is always undefined in the else branch.

The undefined $current_blocks variable will be cast to NULL by PHP when passed into check_replacement() on line 32.

This will cause the sanitization to be skipped entirely for new posts because check_replacement() expects two arrays to be passed.

On strictly typed PHP, this would have always thrown a type error, but in this case, the sanitization is silently skipped for new posts.

/**
 * Check for necessary replacements.
 *
 * @param array  $previous_blocks The previous blocks.
 * @param array  $current_blocks The current blocks.
 * @param string $block_type The block type.
 * @param bool   $modified Whether the blocks have been modified.
 */
public static function check_replacement( $previous_blocks, &$current_blocks, $block_type, &$modified )
{
    if ( ! is_array($current_blocks)) {
        return;
    }
    
    // SANITIZATION HAPPENS HERE.
}

It’s possible to bypass block sanitation in Cwicly, but there’s one saving grace in WordPress Core that accidentally prevents this exploit.

Remember how we said that Cwicly uses opening PHP tags in the eval “sink”?

<?php

$eval = eval(' ?>'.$attributes['code'].'<?php ');

For this to work, we must insert opening PHP tags; otherwise we get a fatal error. The malicious code block has to start with “<?php”.

WordPress runs this code very early on:

function kses_init() {
	kses_remove_filters();

	if ( ! current_user_can( 'unfiltered_html' ) ) {
		kses_init_filters();
	}
}

kses_init_filters() will add hook callbacks that strip out anything looking like HTML/XSS characters when a post is saved and thus, it will remove our opening tag “<” which will cause fatal errors.

This also means that it seems to have never been possible for anybody but admins to use the code block, even if explicitly configured in Cwicly’s role editor.

In any case, this exploit vector didn’t seem to work (we did not check if it’s possible to bypass sanitation in wp_filter_post_kses).

Instead of trying to break the sanitization of wp_kses (which might very well not be possible), we tried to find another way to make WordPress render a registered Gutenberg block.

We found the “Rendered Blocks” endpoint in the WordPress REST API, which seemed promising. This endpoint calls to the WP_REST_Block_Renderer_Controller class which contains the following get_item method:

public function get_item( $request ) {
    global $post;
    
    $post_id = isset( $request['post_id'] ) ? (int) $request['post_id'] : 0;
    
    if ( $post_id > 0 ) {
        $post = get_post( $post_id );
        
        // Set up postdata since this will be needed if post_id was set.
        setup_postdata( $post );
    }
    
    $registry   = WP_Block_Type_Registry::get_instance();
    $registered = $registry->get_registered( $request['name'] );
    
    if ( null === $registered || ! $registered->is_dynamic() ) {
        return new WP_Error(
            'block_invalid',
            __( 'Invalid block.' ),
            array(
                'status' => 404,
            )
        );
    }
    
    $attributes = $request->get_param( 'attributes' );
    
    // Create an array representation simulating the output of parse_blocks.
    $block = array(
        'blockName'    => $request['name'],
        'attrs'        => $attributes,
        'innerHTML'    => '',
        'innerContent' => array(),
    );
    
    // Render using render_block to ensure all relevant filters are used.
    $data = array(
        'rendered' => render_block( $block ),
    );
    
    return rest_ensure_response( $data );
}

After a some validation/input-checking, this method will call the render_block function with entirely user-supplied arguments from the request.

Crucially, this endpoint does not call the wp_kses functions (accidentally) and it’s thus possible to make WordPress/Cwicly render Cwicly’s code block with arbitrary input code.

The last hurdle is to bypass the permission callback of the get_item method, which is the following one:

	public function get_item_permissions_check( $request ) {
		global $post;

		$post_id = isset( $request['post_id'] ) ? (int) $request['post_id'] : 0;

		if ( $post_id > 0 ) {
			$post = get_post( $post_id );

			if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) {
				return new WP_Error(
					'block_cannot_read',
					__( 'Sorry, you are not allowed to read blocks of this post.' ),
					array(
						'status' => rest_authorization_required_code(),
					)
				);
			}
		} else {
			if ( ! current_user_can( 'edit_posts' ) ) {
				return new WP_Error(
					'block_cannot_read',
					__( 'Sorry, you are not allowed to read blocks as this user.' ),
					array(
						'status' => rest_authorization_required_code(),
					)
				);
			}
		}

		return true;
	}

There are two ways to pass the permission check.

1. Line 6-16: If a user can edit a specific (user-supplied) post.

This might be the case for some plugins that allow frontend-submissions of posts and similar functionality, it’s hard to say.

2. Line 19-27: If no post ID is supplied, the user must have the edit_posts capability. This, by default, is anybody with a contributor+ role.

We won’t show an exploit script, but rather show that we can run arbitrary code on the server:

Our used demo script already contains the credentials of a contributor account on the test site and will create a new test.php file that outputs the phpinfo() of the site.

Proposed patch


Anything that involves eval is extremely dangerous and, in our opinion, should not exist to begin with.

Source: The PHP manual

However, removing the code block functionality retrospectively was not an option in this case, since customers already rely on it.

Therefore, it must be ensured that under no circumstances, untrusted code is evaluated that is not known to have been created by a user with the appropriate privilege levels.

This goal is twofold:

  1. Nobody must be able to create posts containing code blocks if they don’t have the configured permissions.
  2. Cwicly’s code block must never evaluate/render anything if the block attributes are not known to be trusted.

Given that Cwicly has no control over how/when WordPress Core might render blocks, the best way to ensure this is with cryptographic signatures.

Upon saving a post, the plugin must check for the appropriate permissions of the user, and then generate a signature block’s code attribute and store the signature in the block’s attributes.

Then, during the render callback of the code block, the signature is checked again to see if the code block was created by a trusted user.

This could look like this:

// Before saving the post contents.
$attributes['signature'] = hash_hmac('sha256', $attributes['code'], wp_salt());

// During rendering of the block.
if (!hash_equals($attributes['signature'], hash_hmac('sha256', $attributes['code'], wp_salt()))) {
    // Attacker - abort.
}{
    // Legitimate user - render block.
}

This ensures that nobody can render a code block via the REST API – assuming they don’t have the hash key.

The above pseudocode uses WordPress Salts which are stored as plaintext in the file system. This is not ideal since they could be leaked via other vulnerabilities/backups, which would then re-introduce the vulnerability because an attacker can generate signatures themselves.

Storing secret keys is a very hard problem. We believe these are acceptable options (from most ideal to least ideal in the context of WordPress):

  1. Supplying the secret directly to the $_SERVER env (i.e. fastcgi_param)
  2. Storing a local path in the $_SERVER env and have the plugin read the file contents.
    This works great with docker-based setups and docker secrets.
  3. Store the “real” key encrypted in the database, and use the WordPress salts as the encryption key.
  4. Using a PHP constant defined before WP loads (NOT in the wp-config.php) file so that it does not end up in backups.

Timeline


Vendor contactedFebruary 13, 2024 (Snicco contacts Cwicly)
First ResponseFebruary 13, 2024
Fully patched atFebruary 14, 2024
Publicly disclosedFebruary 15, 2024
Details disclosedFebruary 23, 2024

Miscellaneous


The Cwicly team showed exceptional cooperation and resolved the issue within two days after disclosure using our recommended patches.

Leave a Reply

Your email address will not be published. Required fields are marked *