In October of this year, we received a report from ngocnb and khuyenn from GiaoHangTietKiem JSC covering a SQL injection vulnerability in WordPress. The bug could allow an attacker to expose data stored in a connected database. This vulnerability was recently addressed as CVE-2021-21661 (ZDI-22-220). This blog covers the root cause of the bug and looks at how the WordPress team chose to address it. First, here’s a quick video demonstrating the vulnerability:
The Vulnerability
The vulnerability occurs in the WordPress Query (WP_Query) class. The WP_Query object is used to perform custom queries to the WordPress database. This object is used by plugins and themes to create their custom display of posts.
The vulnerability occurs when a plugin uses the vulnerable class. One such plugin is Elementor Custom Skin. For this post, we tested the vulnerability against WordPress version 5.8.1 and Elementor Custom Skin plugin version 3.1.3.
In this plugin, the vulnerable WP_Query class is utilized in the get_document_data method of ajax-pagination.php:
Figure 1- - wordpress/wp-content/plugins/ele-custom-skin/includes/ajax-pagination.php
The get_document_data method is invoked when a request is sent to wp-admin/admin-ajax.php and the action parameter is ecsload.
Figure 2 - wordpress/wp-admin/admin-ajax.php
The admin-ajax.php page checks whether the request was made by an authenticated user. If the request came from a non-authenticated user, admin-ajax.php calls a non-authenticated Ajax action. Here, the request is sent without authentication so that the non-authenticated Ajax action is called, which is wp_ajax_nopriv_ecsload.
Searching for the string “wp_ajax_nopriv_ecsload” shows that it is a hook name present in the ajax-pagination.php page:
Figure 3 - wordpress/wp-content/plugins/ele-custom-skin/includes/ajax-pagination.php
The wp_ajax_nopriv_ecsload hook name refers to the get_document_data callback function. This means that the do_action method calls the get_document_data method.
The get_document_data method creates a WP_Query object. The initialization of the WP_Query object calls the following get_posts method:
Figure 4 - wordpress/wp-includes/class-wp-query.php
The get_posts method first parses the user-supplied parameters. Next, it calls the get_sql method which eventually calls get_sql_for_clause to create clauses of the SQL statement from the user-supplied data. get_sql_for_clause calls clean_query to validate the user-supplied string. However, the method fails to validate the terms parameter if the taxonomy parameter is empty and the value of the field parameter is the string “term_taxonomy_id”. The value of the terms parameter is later used in the SQL statement.
Figure 5 - wordpress/wp-includes/class-wp-tax-query.php
Note that the sql variable returned by get_sql() is appended to an SQL SELECT statement and assembled using strings returned from the WP_Tax_Query->get_sql()
method. Later, in the get_posts method, this query is executed by $wpdb->get_col() method, where an SQL injection condition occurs.
This vulnerability can be exploited to read the WordPress database:
The Patch
The patch to address CVE-2022-21661 adds some additional checks to the terms parameter to help prevent further SQL injections from occurring.
Conclusion
Active attacks on WordPress sites often focus on optional plugins rather than the core of WordPress itself. That was the case earlier this year when a bug in the Fancy Product Designer plugin was reported as being under active attack. Similarly, a file upload vulnerability in the Contact Form 7 plugin was also detected as being exploited by Trend Micro sensors. In this case, the bug is exposed through plugins, but exists within WordPress itself. While this is a matter of information disclosure rather than code execution, the data exposed could prove valuable for attackers. It would not surprise us to see this bug in active attacks in the near future. We recommend applying the patch or taking other remedial action as soon as possible. Special thanks to ngocnb and khuyenn from GiaoHangTietKiem JSC for reporting this to the ZDI. You can read their analysis of the bug here.