The vulnerability in the Bricks Builder Theme was originally reported by snicco to the Patchstack bug bounty program for WordPress. We are collaborating with the researcher to release the content of this security advisory article.
This blog post is about the Bricks Builder Theme vulnerability. If you’re a Bricks Builder Theme user, please update the plugin to at least version 1.9.6.1.
You can sign up for the Patchstack Community plan to be notified about vulnerabilities as soon as they become disclosed. Patchstack users with protection enabled have been protected from this vulnerability.
For plugin developers, we have security audit services and Threat Intelligence Feed API for hosting companies.
What to do when you have a vulnerable plugin installed?
- Check if there’s an available update for the plugin. Developers often release patches to fix vulnerabilities. Update to the latest version as soon as possible.
- If an update isn’t available or if the vulnerability poses a significant risk remove the plugin from your site. This will prevent any potential exploitation until a fix is available.
- Enable protection from your Patchstack account, so this and future vulnerabilities would get automatic virtual patches and they would not be exploited.
About the Bricks Builder theme
The theme Bricks Builder (premium version) is estimated to have around 25,000 currently active installations. The Bricks Builder theme is known as the more popular premium site builder theme.
Bricks Builder theme is proclaimed to be an innovative, community-driven, visual site builder for WordPress. This theme enables users to design unique, performant, and scalable websites with a Code-free approach.
The security vulnerability
This plugin suffers from an unauthenticated Remote Code Execution (RCE) vulnerability. This vulnerability allows any unauthenticated user to execute arbitrary PHP code on the WordPress site. The described vulnerability was fixed in version 1.9.6.1 and assigned CVE-2024-25600.
Unauthenticated RCE
The underlying vulnerable code exists in the prepare_query_vars_from_settings
function:
public static function prepare_query_vars_from_settings( $settings = [], $fallback_element_id = '' ) {
$query_vars = $settings['query'] ?? [];
// Some elements already built the query vars. (carousel, related-posts) (@since 1.9.3)
if ( isset( $query_vars['bricks_skip_query_vars'] ) ) {
return $query_vars;
}
// Unset infinite scroll
if ( isset( $query_vars['infinite_scroll'] ) ) {
unset( $query_vars['infinite_scroll'] );
}
// Unset isLiveSearch (@since 1.9.6)
if ( isset( $query_vars['is_live_search'] ) ) {
unset( $query_vars['is_live_search'] );
}
// Do not use meta_key if orderby is not set to meta_value or meta_value_num (@since 1.8)
if ( isset( $query_vars['meta_key'] ) ) {
$orderby = isset( $query_vars['orderby'] ) ? $query_vars['orderby'] : '';
if ( ! in_array( $orderby, [ 'meta_value', 'meta_value_num' ] ) ) {
unset( $query_vars['meta_key'] );
}
}
$object_type = self::get_query_object_type();
$element_id = self::get_query_element_id();
/**
* Use PHP editor
*
* Returns PHP array with query arguments
*
* Supported if 'objectType' is 'post', 'term' or 'user'.
* No merge query.
*
* @since 1.9.1
*/
if ( isset( $query_vars['useQueryEditor'] ) && ! empty( $query_vars['queryEditor'] ) && in_array( $object_type, [ 'post','term','user' ] ) ) {
$post_id = Database::$page_data['preview_or_post_id'];
$php_query_raw = bricks_render_dynamic_data( $query_vars['queryEditor'], $post_id );
$query_vars['posts_per_page'] = get_option( 'posts_per_page' );
// Define an anonymous function that simulates the scope for user code
$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
};
---------------------- CUTTED HERE ----------------------
Notice that there is an eval
function call with $php_query_raw
supplied as the parameter. The $php_query_raw
itself is constructed from $query_vars['queryEditor']
value that will be processed first by bricks_render_dynamic_data
function.
The prepare_query_vars_from_settings
function itself can be called from several processes in the code. One of it is coming from __construct
function inside the Query
class:
public function __construct( $element = [] ) {
$this->register_query();
$this->element_id = ! empty( $element['id'] ) ? $element['id'] : '';
// Check for stored query in query history (@since 1.9.1)
$query_instance = self::get_query_by_element_id( $this->element_id );
if ( $query_instance ) {
// Assign the history query instance properties to this instance, avoid running the query again
foreach ( $query_instance as $key => $value ) {
if ( $key === 'id' ) {
continue;
}
$this->$key = $value;
}
} else {
$this->object_type = ! empty( $element['settings']['query']['objectType'] ) ? $element['settings']['query']['objectType'] : 'post';
// Remove object type from query vars to avoid future conflicts
unset( $element['settings']['query']['objectType'] );
$this->settings = ! empty( $element['settings'] ) ? $element['settings'] : [];
// STEP: Set the query vars from the element settings (@since 1.8)
$this->query_vars = self::prepare_query_vars_from_settings( $this->settings );
---------------------- CUTTED HERE ----------------------
This Query
class also could be called from several processes in the code. If we trace back the process call of one of the cases, we can see that this class first can be called from function render_element
(“includes/api.php”) => function render_element
(“includes/ajax.php”) => function init
(“includes/elements/base.php”) => function render()
(“includes/elements/posts.php”) => new Query()
If we check function render_element
(“includes/api.php”), it is actually a function to handle one of the registered REST API endpoints:
public function rest_api_init_custom_endpoints() {
// 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' ],
]
);
---------------------- CUTTED HERE ----------------------
The API endpoint is already protected by render_element_permissions_check
function that is applied as the function to check permission on the permission_callback
parameter. Let’s see how the function handles the permission:
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;
}
The function only checks for a nonce value and no proper permission or role check is applied. This bricks-nonce value itself is publicly available on the front end of a WordPress site.
With this condition, any unauthenticated user can fetch the nonce and trigger the Remote Code Execution (RCE).
Note that this vulnerability can be reproduced with an Unauthenticated user with the default installation configuration of the theme.
Disclosure note
We decided to release the technical advisory article on this vulnerability early since we have detected active exploitation in our monitoring system. This Bricks Builder Theme vulnerability is currently being exploited and we are seeing attacks from several IP addresses, most of the attacks are from the following IP addresses:
- 200.251.23.57
- 92.118.170.216
- 103.187.5.128
- 149.202.55.79
- 5.252.118.211
- 91.108.240.52
We are also aware of one of the malware that is specifically used on a post-exploitation process of this vulnerability. This malware has a built-in feature to disable some of the security-related plugins such as Wordfence and Sucuri.
The patch
The patch includes an implementation of a permission check on the render_element_permissions_check
function. The patch also implements a sanitization on the $php_query_raw
using the Helpers::sanitize_element_php_code
function. The patch can be seen below:
Conclusion
In general, we recommend not to utilize the eval function on a plugin and theme development process. If the process still needs an eval function to be processed, we recommend applying a very strict permission or role check when accessing the feature and also applying filtering and sanitization on the supplied PHP code.
Timeline
Help us make the Internet a safer place
Making the WordPress ecosystem more secure is a team effort, and we believe that plugin developers and security researchers should work together.
- If you’re a plugin developer, join our mVDP program which makes it easier to report, manage, and address vulnerabilities in your software.
- If you’re a security researcher, join the Patchstack Alliance community and report vulnerabilities to our bug bounty program to earn rewards.