漏洞概述
Essential Addonsfor Elementor是一款功能强大、易于使用的WordPress插件,为使用Elementor建立网站的用户提供了众多额外的功能和组件,使得用户能够更轻松地创建各种网站元素,并且还提供了许多预设计样式和高级功能,使得用户能够更好地管理和定制网站。此外,通过WordPress官方统计(https://api.wordpress.org/plugins/info/1.0/essential-addons-for-elementor-lite.json)可知,它的安装量超过百万。
近期,其密码重置功能存在授权问题,允许未经认证的远程攻击者实现权限提升。
受影响版本
受影响版本:5.4.0 <= Essential Addons for Elementor <= 5.7.1
漏洞分析
问题关键在于Essential Addons for Elementor的密码重置功能没有验证密码重置密钥,就直接改变了指定用户的密码,也就是说攻击者只需知道用户名,就有可能进行密码重置,并获得对应账户的权限。
这里我们采用打补丁前的最新版本(5.7.1)进行分析,具体链接如下:
https://downloads.wordpress.org/plugin/essential-addons-for-elementor-lite.5.7.1.zip
先从includes/Classes/Bootstrap.php看起,这段代码是在WordPress中添加一个动作,当网站初始化时触发该动作,调用login_or_register_user()
该函数位于includes/Traits/Login_Registration.php
它的作用是处理登录和注册表单的提交数据,并根据提交的数据调用相应的方法。正如漏洞概述所说,问题关键在于密码重置,所以我们继续跟进reset_password(),它做的第一步操作就是检查page_id和widget_id是否为空,若为空则返回”xxx ID is missing”,用作后续的错误处理,所以我们首先要做的就是给这两个id赋值:
接着代码就会检查eael-resetpassword-nonce的值是否为空,若不为空则通过wp_verify_nonce()进行验证。
那么问题来了,nonce是什么?如何获取?
在 WordPress 中,nonce 是一个随机字符串,用于防止恶意攻击和跨站请求伪造(CSRF)攻击,它通常与表单一起使用,在表单提交时添加一个随机值,以确保表单只能被特定的用户或请求使用。而关于nonce如何获取,wp_create_nonce()可以做到这一点。
function wp_create_nonce( $action = -1 ) { $user = wp_get_current_user(); $uid = (int) $user->ID; if ( ! $uid ) { /** This filter is documented in wp-includes/pluggable.php */ $uid = apply_filters( 'nonce_user_logged_out', $uid, $action ); } $token = wp_get_session_token( $action ); $i = wp_nonce_tick( $action ); return substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 ); }
但需要注意的是,攻击者无法通过本地生成的 nonce 绕过受害者站点的wp_verify_nonce() 验证,因为他们不能在本地生成与受害者站点相同的 nonce。不过还有一种方法来获取nonce,直接访问主页即可在localize里找到它。
至于原因,则是它被includes/Classes/Asset_Builder.php::load_commnon_asset()设置在localize_object内:
跟踪localize_object不难发现,它被两个函数调用过:
先看add_inline_js(),它用于加载内联js代码和变量。在前端页面加载时,浏览器将加载该代码,并在js中创建名为 localize 的变量,该变量的值就是localize_objects 数组经wp_json_encode()转换的json字符串。
显然,我们刚才直接访问主页发现的localize就是以json格式呈现的。再看frontend_asset_load(),它用于加载前端资源。
最终上述两个函数都会被init_hook()调用,add_inline_js() 被添加到 wp_footer 中,优先级为100,以便在 WordPress 前端页面的页脚中加载内联js代码和变量。而frontend_asset_load() 被添加到 wp_enqueue_scripts 中,优先级为100,以便在 WordPress 前端页面加载时加载所需的前端资源。
之后就是给eael-pass1、eael-pass2赋值,设置新密码了:
当我们满足了上述前提条件以后,再通过rp_login设置想要攻击的用户即可。此时,代码就会借助get_user_by()
function get_user_by( $field, $value ) { $userdata = WP_User::get_data_by( $field, $value ); if ( ! $userdata ) { return false; } $user = new WP_User(); $user->init( $userdata ); return $user; }
按我们给定的字段,也就是用户名去检索用户信息:
如果这是真实存在的用户,就调用reset_password() 进行密码重置操作:
漏洞复现
通过以上分析,我们仅需知道用户名以及nonce值,再向受害站点发送满足漏洞触发条件的请求体,即可进行漏洞利用。
当然,之前也提到了受影响的版本,所以漏洞利用之前,需要优先进行版本探测,有两个方法:
第一,由于frontend_asset_load()调用了wp_enqueue_style()来加载css文件,WordPress就会自动将版本号添加到url中,这是为了确保浏览器能够正确缓存css文件,以便在文件更新时将旧文件替换为新文件:
function wp_enqueue_style( $handle, $src = '', $deps = array(), $ver = false, $media = 'all' ) { _wp_scripts_maybe_doing_it_wrong( __FUNCTION__, $handle ); $wp_styles = wp_styles(); if ( $src ) { $_handle = explode( '?', $handle ); $wp_styles->add( $_handle[0], $src, $deps, $ver, $media ); } $wp_styles->enqueue( $handle ); }
第四个参数即代表指定样式表的版本号的字符串,如果它有的话,它将作为一个查询字符串添加到URL中,以达到破坏缓存的目的。如果版本号被设置为false,则会自动添加一个与当前安装的WordPress版本相同的版本号。如果设置为null,则不添加版本号。最终生成这样一条url:
https://example.com/front-end/css/view/general.min.css?ver=x.x.x
第二,我们也可以通过/wp-content/plugins/essential-addons-for-elementor-lite/readme.txt进行版本探测。
确认受影响的版本以后,就是搜寻真实用户名。同样有多种方式进行探测:
- 使用 WordPress 的 rss feed 来提取用户名。这是一种用于发布和传递 WordPress 内容的标准格式,包含了站点的最新文章、页面、评论等内容,以及发布时间、作者、分类、标签等相关信息。这些信息可以被其他站点或应用程序订阅和使用,增加当前站点的曝光度。而其中dc:creator标签就包含了作者名称;
- 使用 WordPress 的 rest api 来获取用户名;
- 使用了 Yoast 插件提供的站点图来获取用户名。它较于第一种方法的优势在于只能获取到站点中已发布文章的作者信息。如果站点中还有其他用户没有发布文章,那么这些用户的信息是不会被获取的。
除此之外,WordPress也存在默认用户名(admin),这也是该漏洞最常利用的用户名之一。
做完上述信息收集以后,构造报文即可成功重置任意用户密码:
修复方案
在Essential Addons for Elementor(5.7.2)中,进行reset_password()操作之前,先去做了rp_key的校验。补丁如下: