1.前言
Wordpress是全世界最流行的cms系统,在全球建站系统市场占有量超过四成,在如此大的网站基数下,一个有条件的高危漏洞可能也会影响众多站点。近日,Wordpress官方发布了安全通告,由于不恰当的处理,使用了WP_Query类的插件或者主题可能存在SQL注入漏洞,漏洞编号CVE-2022-21661。WP_Query是Wordpress用于处理复杂请求的一个数据库查询类,在核心框架和多种插件、主题中都有应用,只不过核心框架中的使用不满足漏洞利用条件。
2.漏洞成因
我们从最开始的WP_Query类开始看,定位到文件/wp-includes/class-wp-query.php,在其构造函数__construct()中调用了query()方法,参数为$query。由于中间函数调用的比较繁琐,并且不涉及到具体的利用条件,我们利用如下的的流程来简单说明:
`WP_Query::__construct()->` `WP_Query::query()-> //设置了类属性query_vars的值,并调用了get_posts()` `WP_Query::get_posts()-> //当查询的不是针对现有的某个帖子(类型可以是post、page、attachment)时,$this->is_singular为false,会调用到get_sql()方法。` `WP_Query::get_sql()->` `WP_Query::get_sql_clauses()->` `WP_Query::get_sql_for_query()->` `WP_Query::get_sql_for_clause()->` `WP_Query::clean_query() //满足特定条件时,未对terms参数做过滤`
在get_sql_for_clause中调用了clean_query()方法来校验查询中的参数值,当满足 $query['field'] == 'term_taxonomy_id' 时,会调用transform_query()方法。
跟进到transform_query()方法,同样的由于$query['field'] == 'term_taxonomy_id'条件成立,并且$resulting_field被赋值为term_taxonomy_id,因此599行条件成立会直接返回空值。
clean_query()方法因此就没有起到校验参数值的作用。返回到get_sql_for_clause()方法,可以看到$clause['terms']值在用逗号连接后,直接拼接到IN语句中,最终导致了SQL注入漏洞的产生。
在ZDI(Zero Day Initiative)的博客中,以Elementor Custom Skin插件为例进行了分析,我们也以此插件为例来详细介绍Wordpress加载插件的流程以及如何构造对应的payload。复现环境如下:
`Wordpress 5.8.0``Ele Custom Skin 3.1.4`
安装插件并启用后,我们来分析该插件的漏洞触发点/wp-content/plugins/ele-custom-skin/includes/ajax-pagination.php,在get_document_data()方法创建了WP_Query对象。
$this->query属性在构造函数__construct()中进行了初始化,$_POST['query']在json解码后赋值给了$this->query,数据是可控的。因此,get_document_data()满足了SQL注入触发的两个条件。
那么该如何调用get_document_data方法呢?通过搜索发现,init_ajax()方法将get_document_data()注册为action分别为wp_ajax_ecsload、wp_ajax_nopriv_ecsload。
这里就不得不提到wordpress的两个重要方法add_action()、do_action()。
add_action 可以将我们自定义的函数加到特定的 Hook 上去,等待执行。一般来说,我们只需要执行如下命令即可。
add_action("Hook名","函数名")
do_action 是 WordPress 插件机制非常重要的一环,当程序运行到这个函数时,就会将挂载在这个 Hook 上的所有函数执行一遍。这个函数有两个参数,第一个参数是 Hook 的名称,第二个参数则是具体的参数。
do_action("Hook名", "参数")
因此要触发,只需要找到一个入口文件,既可以加载插件,又可以调用特定的action。通过查询资料和代码搜索,我们发现了wp-admin/admin-ajax.php文件。在文件开始,加载了wp-load.php文件。
通过查询资料,我们发现插件加载的流程如下,在wp-settings.php中会加载active状态的插件。
`index.php` `->wp-blog-header.php` `->wp-load.php` `->wp-config.php` `->wp-settings.php`` ``// wp-setting.php``// Load active plugins.``foreach ( wp_get_active_and_valid_plugins() as $plugin ) {` `wp_register_plugin_realpath( $plugin );` `include_once $plugin;`` ` `/**` `* Fires once a single activated plugin has loaded.` `*` `* @since 5.1.0` `*` `* @param string $plugin Full path to the plugin's main file.` `*/` `do_action( 'plugin_loaded', $plugin );``}``unset( $plugin );`
因此,admin-ajax.php满足了插件加载的条件,随后获取action参数后会检查当前用户有没有登录,当用户登录并且有action调用权限时,会调用wp_ajax_前缀的action;而当用户没有登录时,则会调用wp_ajax_nopriv_前缀的action。
很幸运的是,我们前面提到ajax-pagination.php注册了两个action分别为wp_ajax_ecsload、wp_ajax_nopriv_ecsload,因此在未登录的状态下仍然可以触发SQL注入漏洞。在wp-config.php中将WP_DEBUG置为true方便查看报错,构造如下的payload触发报错注入。
Wordpress官方已经在5.8.3的代码提交中修复了这个问题(https://github.com/WordPress/WordPress/commit/271b1f60cd3e46548bd8aeb198eb8a923b9b3827),建议用户及时更新。
wp_parse_id_list()方法会对数组的每个元素调用absint()方法转换成非负的int类型,杜绝了SQL注入漏洞的可能。
参考:
https://github.com/WordPress/WordPress/commit/271b1f60cd3e46548bd8aeb198eb8a923b9b3827