Unauthenticated Remote Code Execution – Bricks <= 1.9.6

Affected pluginBricks Builder
Active installsCommercial ~ 25000
Vulnerable version<= 1.9.6
Audited version1.9.6
Fully patched version1.9.6.1
Recommended remediationUpgrade immediately to version to 1.9.6.1 or higher

Description


Bricks <= 1.9.6 is vulnerable to unauthenticated remote code execution (RCE) which means that anybody can run arbitrary commands and take over the site/server.

Proof of concept


Note:

This will be a long POC, you can skip straight ahead to the demonstration.

This vulnerability is a bit more “complex”, so we’ll work our way from the inside out.

The Bricks\Query class is used to manage the rendering of WordPress post queries.

It contains the following vulnerable method:

public static function prepare_query_vars_from_settings( $settings = [], $fallback_element_id = '' )
{
    // CUT OUT FOR CLARITY
    
    $execute_user_code = function () use ( $php_query_raw ) {
        $user_result = null; // Initialize a variable to capture the result of user code
        
        // Capture user code output using output buffering
        ob_start();
        $user_result = eval( $php_query_raw ); // Execute the user code
        ob_get_clean(); // Get the captured output
        
        return $user_result; // Return the user code result
    };
    
    // CUT OUT FOR CLARITY
}

The critical part is Line 10 where $php_query_raw is passed to PHP’s eval function.

This function is extremely dangerous, and to be honest, should never be used.

Image

To exploit this, we need to find a way to make Bricks call the above code with user controlled input for $php_query_raw.

The prepare_query_vars_from_settings method is always called in the constructor of the Bricks\Query class.

Unsurprisingly, this class is used and instantiated in numerous places.

Image 1

It’s out of scope for us to check every single method call, but the one that stands out the immediately is: Bricks\Ajax::render_element($element)

Bricks uses it to display previews of blocks/elements inside the editor.

This method looks roughly like this (we removed irrelevant parts):

$loop_element = ! empty( $element['loopElement'] ) ? $element['loopElement'] : false;
$element      = $element['element'];

if ( ! empty( $loop_element ) ) {
    $query = new Query( $loop_element );
    
    // CUT FOR BREVITY
}

$element_name       = ! empty( $element['name'] ) ? $element['name'] : '';
$element_class_name = isset( Elements::$elements[ $element_name ]['class'] ) ? Elements::$elements[ $element_name ]['class'] : false;

if ( class_exists( $element_class_name ) ) {
    $element_instance = new $element_class_name( $element );
}

The method creates a new instance of Query with the supplied arguments and either creates a Query class directly on line 5.

Alternatively, any of Brick’s builder elements can also be created/rendered on line 14, by omitting the “loopElement” argument and passing the “name” of the element without the .php file.

Many of these element classes will also call new Query() downstream. There’s also a code element which can be used for this exploit, but in this write-up, we’ll focus on the code path in line 5.

Image 2

The method is callable via the admin-ajax.php endpoint and the WordPress Rest API.

Furthermore, it contains the following permission check logic

if ( bricks_is_ajax_call() && isset( $_POST ) ) {
    self::verify_request();
}

elseif ( bricks_is_rest_call() ) {
    // REST API (Permissions checked in the API->render_element_permissions_check())
}

Ajax::verify_request() will check the current user has permissions to access the Bricks builder (sidenote: This is still not ideal since low privilege users might have builder access).

However, if this method is called via the REST API, Ajax::verify_request() is not called.

The code comment:

REST API (Permissions checked in the API->render_element_permissions_check())

indicates that this check if performed inside the permission callback of WP’s rest API.

// Server-side render (SSR) for builder elements via window.fetch API requests
		register_rest_route(
			self::API_NAMESPACE,
			'render_element',
			[
				'methods'             => 'POST',
				'callback'            => [ $this, 'render_element' ],
				'permission_callback' => [ $this, 'render_element_permissions_check' ],
			]
		);

However, inspecting the render_element_permission_check method, we can see that no permission checks are performed.

The method only checks if a request contains a valid nonce, and the WordPress docs clearly state that “nonces should never be relied on for authorization“:

Image 3
public function render_element_permissions_check( $request ) {
    $data = $request->get_json_params();
    
    if ( empty( $data['postId'] ) || empty( $data['element'] ) || empty( $data['nonce'] ) ) {
        return new \WP_Error( 'bricks_api_missing', __( 'Missing parameters' ), [ 'status' => 400 ] );
    }
    
    $result = wp_verify_nonce( $data['nonce'], 'bricks-nonce' );
    
    if ( ! $result ) {
        return new \WP_Error( 'rest_cookie_invalid_nonce', __( 'Cookie check failed' ), [ 'status' => 403 ] );
    }
    
    return true;
}

So, the only left prerequisite is getting our hands on a valid nonce with the action “bricks-nonce”.

Bricks will output a valid nonce for every request in the frontend, even if the user is not authenticated. This can be seen below in the rendered HTML of the site’s homepage.

There’s a script tag which contains a “bricksData” object that, among other things, contains a valid nonce.

Image 4

Demonstration

To conclude:

  1. We have a valid nonce.
  2. We can use that nonce to call a Brick rest API endpoint.
  3. The endpoint will render any of Brick’s builder elements with our supplied input.
  4. The supplied input will ultimately end up inside a call to PHP’s eval function, which can be used to run any random system command.

We won’t show the (complex) exploit script, but rather a demo “that it works”.

This is how our site looks before running the exploit:

Bricks Before

After running the exploit, every page on the site looks like this and displays a static GIF.

Proposed patch


It’s complicated to give a quick fix, since the functionality to eval user input is backed into several parts of the builder backend

In our opinion, eval should NEVER be used since ultimately, it always leads to something bad, and it’s insecure by-design to use it.

Of course, the quick win is adding the correct permission checks to the REST API endpoint. But that still leaves the dangerous functionality around, and it might very well be possible to call it via other means.

It should not be possible for anybody who’s not an admin (and preferable not even admins) to ever pass anything into eval.

At the bare minimum, the two instances in the codebase where Bricks uses eval (the Query class and the code block class) should be completely guarded against unauthorized, non admin-access, and the input must be heavily validated.

We’ve also identified many places where data from the database flows into eval which is also not ideal since it means that anybody that can update the post_meta table (possible via a low-impact vulnerability) will be able to inject system commands into the database which will then be passed to eval.

One solution for this could be storing a signature along with the code that’s to be evaluated using wp_hash(). That way, at runtime, it can be assured that nobody has been able to inject code into the database.

Timeline


Vendor contactedFebruary 10, 2024 (Snicco contacts Bricks)
First ResponseFebruary 10, 2024
Fully patched atFebruary 13, 2024 as per our recommendations.
Publicly disclosedFebruary 13, 2024
Full disclosure of detailsFebruary 19, 2024

Miscellaneous


The vendor has shown exceptional cooperation and fixed the vulnerability with our assistance in one working day (+ two weekend days) according to our recommendations.

6 responses

  1. Why was a public disclosure made so close to the patch being made available?

    1. No idea what you mean, no details have been disclosed anywhere yet.

      1. Bryant Grant

        Probably because your own timeline above clearly says, Publicly disclosed February 13, 2024.

        1. Correct, the Bricks team published their release and announcement on the 13th (a couple of hours BEFORE us).

          Our page here does not give any further information besides what Bricks themselves published, so I don’t understand your point I guess?

  2. Terry Calop

    I’m pretty sure it is something in the download_image method that’s causing the issue.

  3. Pudja Khan

    Very interesting vulnrability, i’m curious about the POC / Write up? is there any poc / write up about the vulnrability?

Leave a Reply

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