Affected plugin | Bricks Builder |
Active installs | Commercial ~ 25000 |
Vulnerable version | <= 1.9.6 |
Audited version | 1.9.6 |
Fully patched version | 1.9.6.1 |
Recommended remediation | Upgrade 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.
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.
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.
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“:
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.
Demonstration
To conclude:
- We have a valid nonce.
- We can use that nonce to call a Brick rest API endpoint.
- The endpoint will render any of Brick’s builder elements with our supplied input.
- 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:
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 contacted | February 10, 2024 (Snicco contacts Bricks) |
First Response | February 10, 2024 |
Fully patched at | February 13, 2024 as per our recommendations. |
Publicly disclosed | February 13, 2024 |
Full disclosure of details | February 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.
Leave a Reply