今年 10 月,我们收到了来自 GiaoHangTietKiem JSC 的 ngocnb 和 khuyenn 的报告,涉及 WordPress 中的 SQL 注入漏洞。该漏洞可能允许攻击者暴露存储在连接数据库中的数据。此漏洞最近被解决为 CVE-2022-21661 ( ZDI-22-220 )。该博客涵盖了该错误的根本原因,并着眼于 WordPress 团队如何选择解决它。首先,这是一个演示该漏洞的快速视频:
漏洞
该漏洞发生在 WordPress Query ( WP_Query ) 类中。WP_Query对象用于对 WordPress 数据库执行自定义查询。插件和主题使用此对象来创建他们的自定义帖子显示。
当插件使用易受攻击的类时,就会出现该漏洞。一个这样的插件是Elementor Custom Skin 。对于这篇文章,我们针对 WordPress 5.8.1 版和Elementor Custom Skin插件 3.1.3 版测试了该漏洞。
在这个插件中,易受攻击的WP_Query类在ajax-pagination.php的get_document_data方法中被利用:
public function get_document_data(){ | |
---|---|
global $wp_query; | |
... | |
$this->query['paged'] = $this->current_page; // we need current(next) page to be loaded | |
$this->query['post_status'] = 'publish'; | |
$wp_query = new \WP_Query($this->query); // <------ [ZDI comment] $this->query contains user-supplied data | |
wp_reset_postdata();//this fixes some issues with some get_the_ID users. | |
if (is_archive()){ | |
$post_id = $theme_id; | |
} |
图1--wordpress/wp-content/plugins/ele-custom-skin/includes/ajax-pagination.php
当请求发送到wp-admin/admin-ajax.php并且操作参数是ecsload时,调用get_document_data方法。
if ( is_user_logged_in() ) { | |
---|---|
... | |
do_action( "wp_ajax_{$action}" ); | |
} else { | |
... | |
/** | |
* Fires non-authenticated Ajax actions for logged-out users. | |
* | |
* The dynamic portion of the hook name, `$action`, refers | |
* to the name of the Ajax action callback being fired. | |
* | |
* @since 2.8.0 | |
*/ | |
do_action( "wp_ajax_nopriv_{$action}" ); //<------ [ZDI comment] this method is called | |
} |
图 2 - wordpress/wp-admin/admin-ajax.php
admin-ajax.php页面检查请求是否由经过身份验证的用户发出。如果请求来自未经身份验证的用户,admin-ajax.php将调用未经身份验证的 Ajax 操作。在这里,请求是在没有身份验证的情况下发送的,因此会调用未经身份验证的 Ajax 操作,即wp_ajax_nopriv_ecsload。
搜索字符串“wp_ajax_nopriv_ecsload”表明它是一个存在于ajax-pagination.php页面中的钩子名称:
public function init_ajax(){ | |
---|---|
//add_action( 'wp_footer',[$this,'get_document_data'],99);// debug line comment it | |
add_action( 'wp_ajax_ecsload', [$this,'get_document_data']); | |
add_action( 'wp_ajax_nopriv_ecsload', [$this,'get_document_data']); // <--- [ZDI comment] hook name mapping | |
} |
图 3 - wordpress/wp-content/plugins/ele-custom-skin/includes/ajax-pagination.php
wp_ajax_nopriv_ecsload挂钩名称是指get_document_data回调函数。这意味着do_action方法调用get_document_data方法。
get_document_data方法创建一个WP_Query对象。WP_Query对象的初始化调用以下get_posts方法:
public function get_posts() { | |
---|---|
global $wpdb; | |
$this->parse_query(); | |
... | |
// Taxonomies. | |
if ( ! $this->is_singular ) { | |
$this->parse_tax_query( $q ); | |
// [ZDI comment] get_sql() returns the clauses of the SQL statement prepared using the user-input | |
$clauses = $this->tax_query->get_sql( $wpdb->posts, 'ID' ); // <----- calls | |
$join .= $clauses['join']; | |
$where .= $clauses['where']; | |
} | |
... | |
if ( $split_the_query ) { | |
// First get the IDs and then fill in the objects. | |
$this->request = "SELECT $found_rows $distinct {$wpdb->posts}.ID FROM {$wpdb->posts} $join WHERE 1=1 $where $groupby $orderby $limits"; | |
/** | |
* Filters the Post IDs SQL request before sending. | |
* | |
* @since 3.4.0 | |
* | |
* @param string $request The post ID request. | |
* @param WP_Query $query The WP_Query instance. | |
*/ | |
$this->request = apply_filters( 'posts_request_ids', $this->request, $this ); | |
$ids = $wpdb->get_col( $this->request ); //<---- [ZDI comment] SQL injection occurs here | |
if ( $ids ) { | |
$this->posts = $ids; | |
$this->set_found_posts( $q, $limits ); | |
_prime_post_caches( $ids, $q['update_post_term_cache'], $q['update_post_meta_cache'] ); | |
} else { | |
$this->posts = array(); | |
} |
图 4 - wordpress/wp-includes/class-wp-query.php
get_posts方法首先解析用户提供的参数。接下来,它调用get_sql方法,该方法最终调用get_sql_for_clause从用户提供的数据创建 SQL 语句的子句。get_sql_for_clause调用clean_query来验证用户提供的字符串。但是,如果分类参数为空且字段参数的值为字符串“term_taxonomy_id” ,则该方法无法验证术语参数。稍后在 SQL 语句中使用terms参数的值。
public function get_sql_for_clause( &$clause, $parent_query ) { | |
---|---|
global $wpdb; | |
$sql = array( | |
'where' => array(), | |
'join' => array(), | |
); | |
$join = ''; | |
$where = ''; | |
$this->clean_query( $clause ); // [ZDI comment] calls the method to validate parameters | |
if ( is_wp_error( $clause ) ) { | |
return self::$no_results; | |
} | |
$terms = $clause['terms']; | |
$operator = strtoupper( $clause['operator'] ); | |
... | |
} | |
$sql['join'][] = $join; | |
$sql['where'][] = $where; | |
return $sql; | |
} | |
... | |
private function clean_query( &$query ) { | |
if ( empty( $query['taxonomy'] ) ) { | |
if ( 'term_taxonomy_id' !== $query['field'] ) { | |
$query = new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); | |
return; | |
} | |
... |
图 5 - wordpress/wp-includes/class-wp-tax-query.php
请注意,get_sql()返回的sql变量附加到 SQL SELECT 语句并使用从该方法返回的字符串进行组装。后来在get_posts方法中,这个查询是通过$wpdb->get_col()方法执行的,这里出现了SQL注入条件。WP_Tax_Query->get_sql()
此漏洞可被利用来读取 WordPress 数据库:
查看完整尺寸
图 6 - PoC 输出
补丁
解决 CVE-2022-21661 的补丁向terms参数添加了一些额外的检查,以帮助防止进一步的 SQL 注入发生。
查看完整尺寸
图 7 - wordpress/wp-includes/class-wp-tax-query.php 的 clean_query 方法
结论
对 WordPress 网站的主动攻击通常集中在可选插件上,而不是 WordPress 本身的核心。今年早些时候的情况就是这样,当时 Fancy Product Designer 插件中的一个错误被报告为受到主动攻击。同样, Contact Form 7中的文件上传漏洞插件也被检测为被趋势科技传感器利用。在这种情况下,错误通过插件暴露,但存在于 WordPress 本身中。虽然这是信息泄露而不是代码执行的问题,但暴露的数据可能对攻击者很有价值。在不久的将来,在主动攻击中看到这个错误并不会让我们感到惊讶。我们建议尽快应用补丁或采取其他补救措施。特别感谢 GiaoHangTietKiem JSC 的 ngocnb 和 khuyenn 向 ZDI 报告此事。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。